diff --git a/.github/.well-known/funding-manifest-urls b/.github/.well-known/funding-manifest-urls new file mode 100644 index 0000000000..8b6ad1e38b --- /dev/null +++ b/.github/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://www.sea-ql.org/funding.json diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 07556991a3..1caa0cadbc 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,10 +1,10 @@ --- name: Bug Report -about: Report a bug or feature flaw -title: '' -labels: '' -assignees: '' - +about: Report a bug or design flaw +title: "" +labels: "" +assignees: "" +type: Bug --- -## Reproducible Example - - - ## Versions - \ No newline at end of file + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8ad0ea0e1c..14f244b04f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: Q & A url: https://github.com/SeaQL/sea-orm/discussions/new?category=q-a @@ -6,3 +6,6 @@ contact_links: - name: SeaQL Discord Server url: https://discord.com/invite/uCPdDXzbdv about: Join our Discord server to chat with others in the SeaQL community! + - name: Professional Support + url: https://github.com/sponsors/SeaQL + about: Startup and Enterprise-tier sponsors enjoy direct communication with the SeaQL team and professional support for SeaORM usage, data engineering, and Rust development. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index c38dfd143c..c181d78f2c 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,10 +1,10 @@ --- name: Feature Request about: Suggest a new feature for this project -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" +type: Feature --- ## Motivation - + ## Proposed Solutions - + -## Current Workarounds +## Additional Information - + diff --git a/.github/workflows/release-bot.yml b/.github/workflows/release-bot.yml index 0e6bfe2962..31ff1d89f1 100644 --- a/.github/workflows/release-bot.yml +++ b/.github/workflows/release-bot.yml @@ -11,12 +11,14 @@ jobs: issues: write pull-requests: write steps: - - uses: billy1624/release-comment-on-pr@master + - name: Commenting on `${{ github.event.release.tag_name }}` release + uses: billy1624/release-comment-on-pr@master with: + release-tag: ${{ github.event.release.tag_name }} token: ${{ github.token }} message: | ### :tada: Released In [${releaseTag}](${releaseUrl}) :tada: - Your participation is what makes us unique; your adoption is what drives us forward. - Thank you all in the Rust community for making SeaQL what it is today. :crab: - You are also invited to shape it's future! Please star our repos, every ⭐ counts! + Huge thanks for the contribution! + This feature has now been released, so it's a great time to upgrade. + Show some love with a ⭐ on our repo, every star counts! diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4fb3e4a412..297beddb1b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,68 +1,68 @@ # GitHub Actions with Conditional Job Running Based on Commit Message -# +# # -------------------------------------------------------------------------------- -# +# # Following jobs will always run -# +# # - `clippy` +# - `rustfmt` +# - `taplo` # - `test` # - `examples` -# +# # Following jobs will be run when no keywords were found in commit message) -# -# - `compile-sqlite` +# # - `sqlite` -# - `compile-mysql` # - `mysql` # - `mariadb` -# - `compile-postgres` # - `postgres` -# +# # Following jobs will be run if keywords `[issues]` were found in commit message -# +# # - Jobs that will always run # - `issues` -# +# # Following jobs will be run if keywords `[cli]` were found in commit message -# +# # - Jobs that will always run # - `cli` -# +# # Following jobs will be run if keywords `[sqlite]` were found in commit message -# +# # - Jobs that will always run -# - `compile-sqlite` +# - `compile` # - `sqlite` -# +# # Following jobs will be run if keywords `[mysql]` were found in commit message -# +# # - Jobs that will always run -# - `compile-mysql` +# - `compile` # - `mysql` # - `mariadb` -# +# # Following jobs will be run if keywords `[postgres]` were found in commit message -# +# # - Jobs that will always run -# - `compile-postgres` +# - `compile` # - `postgres` - name: tests on: pull_request: paths-ignore: - - '**.md' - - '.github/ISSUE_TEMPLATE/**' + - "**.md" + - ".github/ISSUE_TEMPLATE/**" push: paths-ignore: - - '**.md' - - '.github/ISSUE_TEMPLATE/**' + - "**.md" + - ".github/ISSUE_TEMPLATE/**" branches: - master + - 1.*.x - 0.*.x - pr/**/ci + - ci-* concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }} @@ -70,9 +70,12 @@ concurrency: env: CARGO_TERM_COLOR: always + RUSTC_WRAPPER: sccache + SCCACHE_GHA_ENABLED: true + RUSTFLAGS: "-C debuginfo=0" + CARGO_INCREMENTAL: 0 jobs: - init: name: Init runs-on: ubuntu-latest @@ -91,7 +94,7 @@ jobs: contains(steps.git-log.outputs.message, '[issues]') }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - id: git-log @@ -101,118 +104,108 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: clippy + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 - run: cargo clippy --all -- -D warnings - - run: cargo clippy --all --features runtime-async-std-native-tls,sqlx-all -- -D warnings - # Disable clippy checks on `sea-orm-cli` until we upgraded `clap` to v4. https://github.com/clap-rs/clap/issues/4849 - # - run: cargo clippy --manifest-path sea-orm-cli/Cargo.toml -- -D warnings + - run: cargo clippy --all --features runtime-tokio-native-tls,sqlx-all -- -D warnings + - run: cargo clippy --manifest-path sea-orm-cli/Cargo.toml -- -D warnings - run: cargo clippy --manifest-path sea-orm-migration/Cargo.toml -- -D warnings rustfmt: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly components: rustfmt - run: cargo fmt --all -- --check - run: cargo fmt --manifest-path sea-orm-cli/Cargo.toml --all -- --check - run: cargo fmt --manifest-path sea-orm-migration/Cargo.toml --all -- --check - compile-sqlite: - name: Compile SQLite - needs: init - if: >- - ${{ - needs.init.outputs.run-partial == 'false' || - (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-sqlite == 'true') - }} + taplo: + name: Taplo runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - runtime: [async-std] - tls: [native-tls, rustls] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-sqlite-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-sqlite,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run + - uses: mozilla-actions/sccache-action@v0.0.9 + - run: cargo install --locked taplo-cli + - run: taplo fmt --check - compile-mysql: - name: Compile MySQL + compile: + name: Compile (${{ matrix.label }}) needs: init - if: >- - ${{ - needs.init.outputs.run-partial == 'false' || - (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-mysql == 'true') - }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - runtime: [actix] - tls: [native-tls, rustls] + include: + - label: build + kind: build + - label: tokio native-tls mysql + kind: test + features: sqlx-mysql,runtime-tokio-native-tls + - label: tokio rustls mysql + kind: test + features: sqlx-mysql,runtime-tokio-rustls + - label: tokio native-tls postgres + kind: test + features: sqlx-postgres,runtime-tokio-native-tls + - label: tokio rustls postgres + kind: test + features: sqlx-postgres,runtime-tokio-rustls + - label: tokio sqlite + kind: test + features: sqlx-sqlite,runtime-tokio steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v5 with: path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run - - compile-postgres: - name: Compile PostgreSQL - needs: init - if: >- - ${{ - needs.init.outputs.run-partial == 'false' || - (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-postgres == 'true') - }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - runtime: [tokio] - tls: [native-tls, rustls] - steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-compile-${{ matrix.label }}-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - if: matrix.kind == 'build' + run: | + cargo build --no-default-features + cargo build --no-default-features --features seaography + cargo build --features rbac,schema-sync + - if: matrix.kind == 'test' + run: cargo test --test '*' --features tests-features,${{ matrix.features }} --no-run test: name: Unit Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - run: cargo test --workspace --no-run - run: cargo test --workspace + - run: cargo test --lib --features rbac + - run: cargo test --lib --features entity-registry -- registry + - run: cargo test --manifest-path sea-orm-cli/Cargo.toml --no-run - run: cargo test --manifest-path sea-orm-cli/Cargo.toml cli: @@ -221,45 +214,61 @@ jobs: if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-cli == 'true') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo install --path sea-orm-cli --debug - examples-matrix: - name: Examples Matrix - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - id: set-matrix - run: echo "path_matrix=$(find examples -type f -name 'Cargo.toml' -printf '%P\0' | jq -Rc '[ split("\u0000") | .[] | "examples/\(.)" ]')" >> $GITHUB_OUTPUT - outputs: - path_matrix: ${{ steps.set-matrix.outputs.path_matrix }} - examples: name: Examples runs-on: ubuntu-latest - needs: examples-matrix - timeout-minutes: 15 strategy: fail-fast: false - max-parallel: 12 matrix: - path: ${{ fromJson(needs.examples-matrix.outputs.path_matrix) }} + path: + [ + actix_example, + axum_example, + basic, + graphql_example, + jsonrpsee_example, + loco_example, + loco_seaography, + loco_starter, + parquet_example, + poem_example, + proxy_gluesql_example, + quickstart, + react_admin, + rocket_example, + rocket_okapi_example, + salvo_example, + seaography_example, + tonic_example, + ] steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@v4 + - if: ${{ contains(matrix.path, 'tonic_example') }} + uses: arduino/setup-protoc@v3 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: rustfmt - - run: cargo build --manifest-path ${{ matrix.path }} - - run: cargo test --manifest-path ${{ matrix.path }} - - if: ${{ contains(matrix.path, 'core/Cargo.toml') }} - run: cargo test --manifest-path ${{ matrix.path }} --features mock - - uses: dtolnay/rust-toolchain@master + - uses: actions/cache@v5 with: - toolchain: nightly - components: rustfmt - - run: cargo fmt --manifest-path ${{ matrix.path }} --all -- --check + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-examples-${{ matrix.path }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - working-directory: ./examples/ + run: | + find ${{ matrix.path }} -type f -name 'Cargo.toml' -print0 | xargs -t -0 -I {} cargo fmt --manifest-path {} -- --check + find ${{ matrix.path }} -type f -name 'Cargo.toml' -print0 | xargs -t -0 -I {} cargo update --manifest-path {} + find ${{ matrix.path }} -type f -name 'Cargo.toml' -print0 | xargs -t -0 -I {} cargo test --manifest-path {} + ${{'! '}}${{ '[ -d "' }}${{ matrix.path }}${{ '/api" ]' }} || find ${{ matrix.path }}/api -type f -name 'Cargo.toml' -print0 | xargs -t -0 -I {} cargo test --manifest-path {} + - if: matrix.path == 'quickstart' + working-directory: ./examples/${{ matrix.path }} + run: cargo run issues-matrix: name: Issues Matrix @@ -267,7 +276,7 @@ jobs: if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-issues == 'true') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: set-matrix run: echo "path_matrix=$(find issues -type f -name 'Cargo.toml' -printf '%P\0' | jq -Rc '[ split("\u0000") | .[] | "issues/\(.)" ]')" >> $GITHUB_OUTPUT outputs: @@ -285,8 +294,16 @@ jobs: matrix: path: ${{ fromJson(needs.issues-matrix.outputs.path_matrix) }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-issue-${{ matrix.path }} + - uses: mozilla-actions/sccache-action@v0.0.9 - run: cargo build --manifest-path ${{ matrix.path }} - run: cargo test --manifest-path ${{ matrix.path }} @@ -294,7 +311,7 @@ jobs: name: SQLite needs: - init - - compile-sqlite + - compile if: >- ${{ needs.init.outputs.run-partial == 'false' || @@ -306,27 +323,59 @@ jobs: strategy: fail-fast: false matrix: - runtime: [async-std] - tls: [native-tls, rustls] + runtime: [tokio] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-sqlite-tests-sqlite-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - run: cargo test --test '*' --features tests-features,sqlx-sqlite,runtime-${{ matrix.runtime }} --no-run + - run: cargo test --test '*' --features tests-features,sqlx-sqlite,runtime-${{ matrix.runtime }} + - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-sqlite,runtime-${{ matrix.runtime }} --no-run + - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-sqlite,runtime-${{ matrix.runtime }} + + rusqlite: + name: rusqlite + needs: + - init + - compile + if: >- + ${{ + needs.init.outputs.run-partial == 'false' || + (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-sqlite == 'true') + }} + runs-on: ubuntu-latest + env: + DATABASE_URL: "sqlite::memory:" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v5 with: path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-sqlite-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-sqlite,runtime-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-sqlite,runtime-${{ matrix.runtime }}-${{ matrix.tls }} + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-sqlite-tests-rusqlite-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - working-directory: ./sea-orm-sync + run: cargo test --test '*' --features tests-features,rusqlite + - working-directory: ./sea-orm-sync/examples/quickstart + run: cargo run + - working-directory: ./sea-orm-sync/examples/parquet_example + run: cargo run mysql: name: MySQL needs: - init - - compile-mysql + - compile if: >- ${{ needs.init.outputs.run-partial == 'false' || @@ -338,8 +387,8 @@ jobs: strategy: fail-fast: false matrix: - version: [8.0, 5.7] - runtime: [actix] + version: [lts, 5.7] + runtime: [tokio] tls: [native-tls] services: mysql: @@ -350,7 +399,6 @@ jobs: MYSQL_USER: sea MYSQL_PASSWORD: sea MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_ROOT_PASSWORD: ports: - "3306:3306" options: >- @@ -359,24 +407,26 @@ jobs: --health-timeout=5s --health-retries=3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v5 with: path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-mysql-tests-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - run: cargo test --test '*' --features tests-features,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run + - run: cargo test --test '*' --features tests-features,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} + - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} mariadb: name: MariaDB needs: - init - - compile-mysql + - compile if: >- ${{ needs.init.outputs.run-partial == 'false' || @@ -388,44 +438,46 @@ jobs: strategy: fail-fast: false matrix: - version: [10.6] - runtime: [actix] + version: [lts] + runtime: [tokio] tls: [native-tls] services: - mysql: + mariadb: image: mariadb:${{ matrix.version }} env: - MYSQL_HOST: 127.0.0.1 - MYSQL_DB: mysql - MYSQL_USER: sea - MYSQL_PASSWORD: sea - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_ROOT_PASSWORD: + MARIADB_HOST: 127.0.0.1 + MARIADB_DB: mysql + MARIADB_USER: sea + MARIADB_PASSWORD: sea + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes ports: - "3306:3306" options: >- - --health-cmd="mysqladmin ping" + --health-cmd="healthcheck.sh + --connect + --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v5 with: path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-mariadb-tests-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - run: cargo test --test '*' --features tests-features,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run + - run: cargo test --test '*' --features tests-features,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} postgres: name: Postgres needs: - init - - compile-postgres + - compile if: >- ${{ needs.init.outputs.run-partial == 'false' || @@ -437,7 +489,7 @@ jobs: strategy: fail-fast: false matrix: - version: [14.4] + version: [14, 16] runtime: [tokio] tls: [native-tls] services: @@ -455,15 +507,17 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/cache@v3 + - uses: actions/cache@v5 with: path: | - ~/.cargo/registry - ~/.cargo/git - Cargo.lock - target - key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }} - - run: cargo test --test '*' --features default,sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-postgres-tests-${{ matrix.runtime }}-${{ matrix.tls }}-${{ hashFiles('**/Cargo.toml') }} + - uses: mozilla-actions/sccache-action@v0.0.9 + - run: cargo test --test '*' --features tests-features,sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run + - run: cargo test --test '*' --features tests-features,sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} + - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} --no-run - run: cargo test --manifest-path sea-orm-migration/Cargo.toml --test '*' --features sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} diff --git a/.gitignore b/.gitignore index 558044adf4..7ec785cd4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ target +firedbg Cargo.lock *.sublime* .vscode .idea/* -*/.idea/* \ No newline at end of file +*/.idea/* +.env.local +.DS_Store \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000000..8bd779583b --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,6 @@ +format_code_in_doc_comments = true +ignore = [ + # These files are used to test the output of derive macros. + # Formatting them will cause the tests to fail. + "sea-orm-codegen/src/tests_cfg", +] diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000000..8b163cb733 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,8 @@ +include = ["**/*.toml"] + +[formatting] +align_entries = true +allowed_blank_lines = 1 +array_auto_collapse = false +indent_string = ' ' +reorder_keys = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 644766f8ce..fd09f03889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,63 +5,2113 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## 0.12.0 - Pending +## 2.0.0 - pending + +### Release Candidates + +- [2.0.0-rc.37](changelog/2.0.0-rc.37.md) — ER Diagram Generation +- [2.0.0-rc.36](changelog/2.0.0-rc.36.md) — Per-migration transaction control +- [2.0.0-rc.35](changelog/2.0.0-rc.35.md) — SQLite transaction modes, DeriveIntoActiveModel extensions, Decimal64/Bytes, schema sync fix +- [2.0.0-rc.34](changelog/2.0.0-rc.34.md) — Arrow/Parquet support, `try_from_u64` for DeriveValueType +- [2.0.0-rc.32](changelog/2.0.0-rc.32.md) — `MigratorTrait` with `self`, PostgreSQL `application_name` +- [2.0.0-rc.31](changelog/2.0.0-rc.31.md) — `ne_all`, typed `TextUuid`, COUNT overflow fix +- [2.0.0-rc.30](changelog/2.0.0-rc.30.md) — Maintenance release, `sea-query` bump +- [2.0.0-rc.29](changelog/2.0.0-rc.29.md) — Tracing spans, UUID-as-TEXT, relation filtering, LEFT JOIN fix +- [2.0.0-rc.28](changelog/2.0.0-rc.28.md) — `sqlx-all` in migration, `set_if_not_equals_and`, auto_increment for String/Uuid PKs +- [2.0.0-rc.27](changelog/2.0.0-rc.27.md) — `DeriveValueType` implements `NotU8` for PostgreSQL arrays +- [2.0.0-rc.26](changelog/2.0.0-rc.26.md) — `postgres-use-serial-pk` feature for legacy serial PKs +- [2.0.0-rc.25](changelog/2.0.0-rc.25.md) — Value system restoration, `sea-query` bump +- [2.0.0-rc.24](changelog/2.0.0-rc.24.md) — `sea-query` bump to rc.27 +- [2.0.0-rc.23](changelog/2.0.0-rc.23.md) — `DeriveValueType` implements `IntoActiveValue`, remove `NotU8` +- [2.0.0-rc.22](changelog/2.0.0-rc.22.md) — `DatabaseExecutor` unified type, value array refactor +- [2.0.0-rc.21](changelog/2.0.0-rc.21.md) — Rusqlite / `sea-orm-sync` crate, `exists` on PaginatorTrait +- [2.0.0-rc.20](changelog/2.0.0-rc.20.md) — Stringy newtypes, M2M self-ref, nullable columns, bug fixes -+ 2023-05-19: `0.12.0-rc.2` +### New Features + +* Role Based Access Control https://github.com/SeaQL/sea-orm/pull/2683 + + 1. a hierarchical RBAC engine that is table scoped + + a user has 1 (and only 1) role + + a role has a set of permissions on a set of resources + + permissions here are CRUD operations and resources are tables + + but the engine is generic so can be used for other things + + roles have hierarchy, and so can inherit permissions + + there is a wildcard `*` to grant all permissions or resources + + individual users can have rules override + 3. a set of Entities to load / store the access control rules to / from database + 4. a query auditor that dissect queries for necessary permissions (implemented in SeaQuery) + 5. integration of RBAC into SeaORM in form of `RestrictedConnection`. + it implements `ConnectionTrait`, and will audit all queries and perform permission check, + and reject them accordingly. all Entity operations except raw SQL are supported. + complex joins, insert select from, and even CTE queries are supported. +```rust +// load rules from database +db_conn.load_rbac().await?; + +// admin can create bakery +let db = db_conn.restricted_for(admin)?; +let seaside_bakery = bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + ..Default::default() +}; +assert!(Bakery::insert(seaside_bakery).exec(&db).await.is_ok()); + +// public cannot create bakery +let db = db_conn.restricted_for(public)?; +assert!(matches!( + Bakery::insert(bakery::ActiveModel::default()) + .exec(&db) + .await, + Err(DbErr::AccessDenied { .. }) +)); +``` + +* Overhauled `Entity::insert_many`. We've made a number of changes https://github.com/SeaQL/sea-orm/pull/2628 + 1. removed APIs that can panic + 2. new helper struct `InsertMany`, `last_insert_id` is now `Option` + 3. on empty iterator, `None` or `vec![]` is returned on exec operations + 4. `TryInsert` API is unchanged + +Previously, `insert_many` shares the same helper struct with `insert_one`, which led to an awkard API. +```rust +let res = Bakery::insert_many(std::iter::empty()) + .on_empty_do_nothing() // <- you need to add this + .exec(db) + .await; + +assert!(matches!(res, Ok(TryInsertResult::Empty))); +``` +`last_insert_id` is now `Option`: +```rust +struct InsertManyResult +{ + pub last_insert_id: Option< as PrimaryKeyTrait>::ValueType>, +} +``` +Which means the awkardness is removed: +```rust +let res = Entity::insert_many::([]).exec(db).await; + +assert_eq!(res?.last_insert_id, None); // insert nothing return None + +let res = Entity::insert_many([ActiveModel { id: Set(1) }, ActiveModel { id: Set(2) }]) + .exec(db) + .await; + +assert_eq!(res?.last_insert_id, Some(2)); // insert something return Some +``` +Same on conflict API as before: +```rust +let res = Entity::insert_many([ActiveModel { id: Set(3) }, ActiveModel { id: Set(4) }]) + .on_conflict_do_nothing() + .exec(db) + .await; + +assert!(matches!(conflict_insert, Ok(TryInsertResult::Conflicted))); +``` +Exec with returning now returns a `Vec`, so it feels intuitive: +```rust +assert!( + Entity::insert_many::([]) + .exec_with_returning(db) + .await? + .is_empty() // no footgun, nice +); + +assert_eq!( + Entity::insert_many([ + ActiveModel { + id: NotSet, + value: Set("two".into()), + } + ]) + .exec_with_returning(db) + .await + .unwrap(), + [ + Model { + id: 2, + value: "two".into(), + } + ] +); +``` +* Improved utility of `ActiveModel::from_json`. Consider the following Entity https://github.com/SeaQL/sea-orm/pull/2599 +```rust +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, // <- not nullable + pub name: String, +} +``` +Previously, the following would result in error "missing field `id`": +```rust +assert!( + cake::ActiveModel::from_json(json!({ + "name": "Apple Pie", + })).is_err(); +); +``` +Now, the ActiveModel will be partially filled: +```rust +assert_eq!( + cake::ActiveModel::from_json(json!({ + "name": "Apple Pie", + })) + .unwrap(), + cake::ActiveModel { + id: NotSet, + name: Set("Apple Pie".to_owned()), + } +); +``` +* A full `Model` can now be used as `PartialModel` in nested query https://github.com/SeaQL/sea-orm/pull/2642 +```rust +#[derive(DerivePartialModel)] +#[sea_orm(entity = "cake::Entity")] +struct Cake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, +} + +let cake: Cake = cake::Entity::find() + .left_join(bakery::Entity) + .order_by_asc(cake::Column::Id) + .into_partial_model() + .one(&ctx.db) + .await? + .unwrap(); + +assert_eq!(cake.id, 13); +assert_eq!(cake.name, "Cheesecake"); +assert_eq!( + cake.bakery.unwrap(), + bakery::Model { + id: 42, + name: "cool little bakery".to_string(), + } +); +``` +* Wrapper type derived with `DeriveValueType` can now be used as primary key https://github.com/SeaQL/sea-orm/pull/2643 +```rust +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "my_value_type")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: MyInteger, +} + +#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)] +pub struct MyInteger(pub i32); +// only for i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64 +``` +* You can now define unique keys that span multiple columns in Entity https://github.com/SeaQL/sea-orm/pull/2651 +```rust +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "lineitem")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique_key = "item")] + pub order_id: i32, + #[sea_orm(unique_key = "item")] + pub cake_id: i32, +} + +let stmts = Schema::new(backend).create_index_from_entity(lineitem::Entity); + +assert_eq!( + stmts[0], + Index::create() + .name("idx-lineitem-item") + .table(lineitem::Entity) + .col(lineitem::Column::OrderId) + .col(lineitem::Column::CakeId) + .unique() + .take() +); + +assert_eq!( + backend.build(stmts[0]), + r#"CREATE UNIQUE INDEX "idx-lineitem-item" ON "lineitem" ("order_id", "cake_id")"# +); +``` +* Overhauled `ConnectionTrait` API: `execute`, `query_one`, `query_all`, `stream` now takes in SeaQuery statement instead of raw SQL statement https://github.com/SeaQL/sea-orm/pull/2657 +```rust +// old +let query: SelectStatement = Entity::find().filter(..).into_query(); +let backend = self.db.get_database_backend(); +let stmt = backend.build(&query); +let rows = self.db.query_all(stmt).await?; + +// new +let query: SelectStatement = Entity::find().filter(..).into_query(); +let rows = self.db.query_all(&query).await?; +``` +* Added `raw_sql` macro for ergonomic parameter injection +```rust +#[derive(FromQueryResult)] +struct Cake { + name: String, + #[sea_orm(nested)] + bakery: Option, +} + +#[derive(FromQueryResult)] +struct Bakery { + #[sea_orm(alias = "bakery_name")] + name: String, +} + +let cake_ids = [2, 3, 4]; // expanded by the `..` operator + +let cake: Option = Cake::find_by_statement(raw_sql!( + Sqlite, + r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name" + FROM "cake" + LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id" + WHERE "cake"."id" IN ({..cake_ids})"# +)) +.one(db) +.await?; +``` +* Added `consolidate` method to `SelectThree`. This output has different shape depending on the topology of the join. +```rust +// Order -> Customer +// -> Lineitem + +let items: Vec<(order::Model, Option, Option)> = + order::Entity::find() + .find_also_related(customer::Entity) + .find_also_related(lineitem::Entity) + .order_by_asc(order::Column::Id) + .order_by_asc(lineitem::Column::Id) + .all(&ctx.db) + .await?; + +// flat result +assert_eq!( + items, + vec![ + (order, Some(customer), Some(line_1)), + (order, Some(customer), Some(line_2)), + ] +); + +let items: Vec<(order::Model, Vec, Vec)> = + order::Entity::find() + .find_also_related(customer::Entity) + .find_also_related(lineitem::Entity) + .order_by_asc(order::Column::Id) + .order_by_asc(lineitem::Column::Id) + .consolidate() // <- + .all(&ctx.db) + .await?; + +// consolidated by order +assert_eq!( + items, + vec![( + order, + vec![customer], + vec![line_1, line_2] + )] +); + +// Order -> Lineitem -> Cake +let items: Vec<(order::Model, Option, Option)> = + order::Entity::find() + .find_also_related(lineitem::Entity) + .and_also_related(cake::Entity) + .order_by_asc(order::Column::Id) + .order_by_asc(lineitem::Column::Id) + .all(&ctx.db) + .await?; + +// flat result +assert_eq!( + items, + vec![ + (order, Some(line_1), Some(cake_1)), + (order, Some(line_2), Some(cake_2)), + ] +); + +let items: Vec<(order::Model, Vec<(lineitem::Model, Vec)>)> = + order::Entity::find() + .find_also_related(lineitem::Entity) + .and_also_related(cake::Entity) + .order_by_asc(order::Column::Id) + .order_by_asc(lineitem::Column::Id) + .consolidate() // <- + .all(&ctx.db) + .await?; + +// consolidated by order first, then by line +assert_eq!( + items, + vec![( + order, + vec![(line_1, vec![cake_1]), (line_2, vec![cake_2])] + )] +); +``` +* Added `Select::has_related` +```rust +// cake -> fruit: find all cakes containing mango +assert_eq!( + cake::Entity::find() + .has_related(fruit::Entity, fruit::Column::Name.eq("Mango")) + .build(DbBackend::Sqlite) + .to_string(), + [ + r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + r#"WHERE EXISTS(SELECT 1 FROM "fruit""#, + r#"WHERE "fruit"."name" = 'Mango'"#, + r#"AND "cake"."id" = "fruit"."cake_id")"#, + ] + .join(" ") +); +// cake -> cake_filling -> filling: find all cakes with orange fillings +assert_eq!( + cake::Entity::find() + .has_related(filling::Entity, filling::Column::Name.eq("Marmalade")) + .build(DbBackend::Sqlite) + .to_string(), + [ + r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + r#"WHERE EXISTS(SELECT 1 FROM "filling""#, + r#"INNER JOIN "cake_filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"WHERE "filling"."name" = 'Marmalade'"#, + r#"AND "cake"."id" = "cake_filling"."cake_id")"#, + ] + .join(" ") +); +``` +* Support self-referencing relations in loader +```rust +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "staff")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub reports_to_id: Option, + #[sea_orm(self_ref, relation_enum = "ReportsTo", from = "reports_to_id", to = "id")] + pub reports_to: HasOne, +} + +// Entity Loader +let staff = staff::Entity::load() + .with(staff::Relation::ReportsTo) + .all(db) + .await?; + +assert_eq!(staff[0].name, "Alan"); +assert_eq!(staff[0].reports_to, None); + +assert_eq!(staff[1].name, "Ben"); +assert_eq!(staff[1].reports_to.as_ref().unwrap().name, "Alan"); + +assert_eq!(staff[2].name, "Alice"); +assert_eq!(staff[2].reports_to.as_ref().unwrap().name, "Alan"); + +// Model Loader +let staff = staff::Entity::find().all(db).await?; + +let reports_to = staff + .load_self(staff::Entity, staff::Relation::ReportsTo, db) + .await?; + +assert_eq!(staff[0].name, "Alan"); +assert_eq!(reports_to[0], None); + +assert_eq!(staff[1].name, "Ben"); +assert_eq!(reports_to[1].unwrap().name, "Alan"); + +assert_eq!(staff[2].name, "Alice"); +assert_eq!(reports_to[2].unwrap().name, "Alan"); +``` +* Strongly-typed column https://github.com/SeaQL/sea-orm/pull/2794 +```rust +// old +user::Entity::find().filter(user::Column::Name.contains("Bob")) + +// new +user::Entity::find().filter(user::COLUMN.name.contains("Bob")) + +// compile error: the trait `From<{integer}>` is not implemented for `String` +user::Entity::find().filter(user::COLUMN.name.like(2)) +``` +* Unix timestamp column type that will be mapped to big integer in database +```rust +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "access_log")] +pub struct Model { + .. // with `chrono` crate + pub ts: ChronoUnixTimestamp, + pub ms: ChronoUnixTimestampMillis, + .. // with `time` crate + pub ts: TimeUnixTimestamp, + pub ms: TimeUnixTimestampMillis, +} +``` +* Nested ActiveModel (ActiveModelEx) and cascade operations https://github.com/SeaQL/sea-orm/pull/2818 + + The following operation saves a new set of user + profile + post + tag + post_tag into the database atomically: +```rust +let user = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("image.jpg")) + .add_post( + post::ActiveModel::builder() + .set_title("Nice weather") + .add_tag(tag::ActiveModel::builder().set_tag("sunny")), + ) + .save(db) + .await?; +``` + +### Enhancements + +* [sea-orm-cli] Added `--column-extra-derives` https://github.com/SeaQL/sea-orm/pull/2212 +* [sea-orm-cli] Added `--big-integer-type=i32` to use i32 for bigint (for SQLite) +* [sea-orm-cli] Fix codegen to not generate relations to filtered entities https://github.com/SeaQL/sea-orm/pull/2913 +* [sea-orm-cli] Added `--experimental-preserve-user-modifications` https://github.com/SeaQL/sea-orm/pull/2755 https://github.com/SeaQL/sea-orm/pull/2964 +* Added `Model::try_set` +* Added new error variant `BackendNotSupported`. Previously, it panics with e.g. "Database backend doesn't support RETURNING" https://github.com/SeaQL/sea-orm/pull/2630 +```rust +let result = cake::Entity::insert_many([]) + .exec_with_returning_keys(db) + .await; + +if db.support_returning() { + // Postgres and SQLite + assert_eq!(result.unwrap(), []); +} else { + // MySQL + assert!(matches!(result, Err(DbErr::BackendNotSupported { .. }))); +} +``` +* Added new error variant `PrimaryKeyNotSet`. Previously, it panics with "PrimaryKey is not set" https://github.com/SeaQL/sea-orm/pull/2627 +```rust +assert!(matches!( + Update::one(cake::ActiveModel { + ..Default::default() + }) + .exec(&db) + .await, + Err(DbErr::PrimaryKeyNotSet { .. }) +)); +``` +* Remove panics in `Schema::create_enum_from_active_enum` https://github.com/SeaQL/sea-orm/pull/2634 +```rust +fn create_enum_from_active_enum(&self) -> Option +// method can now return None +``` +* Added `ColumnTrait::eq_any` as a shorthand for the ` = ANY` operator. Postgres only. +```rust +assert_eq!( + cake::Entity::find() + .filter(cake::Column::Id.eq_any(vec![4, 5])) + .build(DbBackend::Postgres) + .to_string(), + r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = ANY(ARRAY [4,5])"# +); +``` +* Added `ActiveModelTrait::try_set` +```rust +pub trait ActiveModelTrait { + /// old: set the Value of a ActiveModel field, panic if failed + fn set(&mut self, c: ::Column, v: Value) { + self.try_set(c, v).unwrap_or_else(|e| panic!(..)) + } + + /// new: same as above but non-panicking + fn try_set(&mut self, c: ::Column, v: Value) -> Result<(), DbErr>; +} +``` +* `Linked` can now be used in partial select, in case `Related` cannot be defined +```rust +pub struct ToBakery; +impl Linked for ToBakery { + type FromEntity = super::cake::Entity; + type ToEntity = super::bakery::Entity; + + fn link(&self) -> Vec { + vec![Relation::Bakery.def()] + } +} + +#[derive(Debug, DerivePartialModel)] +#[sea_orm(entity = "cake::Entity", into_active_model)] +struct Cake2 { + id: i32, + name: String, + #[sea_orm(nested, alias = "r0")] + bakery: Option, + #[sea_orm(skip)] + ignore: Ignore, +} + +let cake2: Cake2 = cake::Entity::find() + .left_join_linked(ToBakery) + .order_by_asc(cake::Column::Id) + .into_partial_model() + .one(&ctx.db) + .await? + .unwrap(); +``` +* `RelationDef` now implements `Clone`. `on_condition` is changed to `Arc` but this is a minor breaking change. +* Added `extra` on column attribute: +```rust +#[cfg(feature = "with-rust_decimal")] +#[sea_orm(extra = "CHECK (price > 0)")] +pub price: Decimal, + +// results in: +ColumnDef::new("price") + .decimal() + .not_null() + .extra("CHECK (price > 0)"), +``` +* Added `ColumnTrait::avg`, in addition to `sum`, `min`, `max` etc +```rust +let average: Decimal = order::Entity::find() + .select_only() + .column_as(order::Column::Total.avg(), "avg") + .into_tuple() + .one(&ctx.db) + .await? + .unwrap(); +``` +* `SchemaBuilder::sync` can now be used in migrations +```rust +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.get_schema_builder() + .register(note::Entity) + .sync(db) + .await + } +} +``` +* Allowed None for `max_lifetime` and `idle_timeout` Parameters https://github.com/SeaQL/sea-orm/pull/2748 +* Try to parse `u32` in Postgres as `i32` https://github.com/SeaQL/sea-orm/pull/2753 +* `DeriveActiveEnum` now also impl `IntoActiveValue` https://github.com/SeaQL/sea-orm/issues/1972 +* `DeriveValueType` now also supports any structs that can be converted to / from string https://github.com/SeaQL/sea-orm/issues/2811 +```rust +#[derive(Copy, Clone, Debug, PartialEq, Eq, DeriveValueType)] +#[sea_orm(value_type = "String")] +pub struct Tag3 { + pub i: i64, +} + +impl std::fmt::Display for Tag3 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.i) + } +} + +impl std::str::FromStr for Tag3 { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + let i: i64 = s.parse()?; + Ok(Self { i }) + } +} +``` +* Fix `DeriveIntoActiveModel` on `Option` fields https://github.com/SeaQL/sea-orm/pull/2926 +```rust +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "fruit")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub cake_id: Option, +} + +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "::ActiveModel")] +struct PartialFruit { + cake_id: Option, +} + +assert_eq!( + PartialFruit { cake_id: Some(1) }.into_active_model(), + fruit::ActiveModel { id: NotSet, name: NotSet, cake_id: Set(Some(1)) } +); + +assert_eq!( + PartialFruit { cake_id: None }.into_active_model(), + fruit::ActiveModel { id: NotSet, name: NotSet, cake_id: NotSet } +); +``` +* `FromQueryResult` now supports nullable nested model https://github.com/SeaQL/sea-orm/pull/2845 +```rust +#[derive(FromQueryResult)] +struct CakeWithOptionalBakeryModel { + #[sea_orm(alias = "cake_id")] + id: i32, + #[sea_orm(alias = "cake_name")] + name: String, + #[sea_orm(nested)] + bakery: Option, // can be null +} +``` +* Added `try_from_u64` to `DeriveValueType` https://github.com/SeaQL/sea-orm/pull/2958 +```rust +// Test for try_from_u64 attribute with type alias +type UserId = i32; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)] +#[sea_orm(try_from_u64)] +pub struct MyUserId(pub UserId); +``` +* Arrow / Parquet support https://github.com/SeaQL/sea-orm/pull/2957 + + Added `ArrowSchema`, `DeriveArrowSchema` + + Support decimal with different formats + + Support timestamp with different timezone / resolution + + Added parquet example + +### Breaking Changes + +Please read [SeaQuery's breaking changes](https://github.com/SeaQL/sea-query/blob/master/CHANGELOG.md#breaking-changes) as well. But for most compile errors, you can simply add `use sea_orm::ExprTrait;` in scope. +```rust +error[E0599]: no method named `like` found for enum `sea_query::Expr` in the current scope + | + | Expr::col((self.entity_name(), *self)).like(s) + | + | fn like(self, like: L) -> Expr + | ---- the method is available for `sea_query::Expr` here + | + = help: items from traits can only be used if the trait is in scope +help: trait `ExprTrait` which provides `like` is implemented but not in scope; perhaps you want to import it + | + -> + use sea_orm::ExprTrait; +``` +```rust +error[E0308]: mismatched types + --> src/sqlite/discovery.rs:27:57 + | + | .and_where(Expr::col(Alias::new("type")).eq("table")) + | -- ^^^^^^^ expected `&Expr`, found `&str` + | | + | arguments to this method are incorrect + | + = note: expected reference `&sea_query::Expr` + found reference `&'static str` +``` +```rust +error[E0308]: mismatched types + | +390 | Some(Expr::col(Name).eq(PgFunc::any(query.symbol))) + | -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&Expr`, found `FunctionCall` + | | + | arguments to this method are incorrect + | +note: method defined here + --> /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/core/src/cmp.rs:254:8 +``` +```rust +error[E0277]: the trait bound `sea_orm::Condition: From` is not satisfied + | +367 | .add_option(option) + | ---------- ^^^^^^ the trait `From` is not implemented for `sea_orm::Condition` + | | + | required by a bound introduced by this call + | + = note: required for `bool` to implement `Into` +``` + +* Removed `runtime-actix` feature flag. It's been an alias of `runtime-tokio` for more than a year, so there should be no impact. +* Enabled `sqlite-use-returning-for-3_35` by default. SQLite `3.35` was released in 2021, it should be the default by now. +* Now implemented `impl PartialModelTrait for T`, there may be a potential conflict https://github.com/SeaQL/sea-orm/pull/2642 +* Now `DeriveValueType` will also `TryFromU64` if applicable, there may be a potential conflict https://github.com/SeaQL/sea-orm/pull/2643 +* Now `DeriveValueType` also impl `IntoActiveValue` and `NotU8`, there may be a potential conflict +* Added `TryIntoModel` and `Serialize` to trait bounds of `ActiveModel::from_json`. There should be no impact if your models are derived with `DeriveEntityModel` https://github.com/SeaQL/sea-orm/pull/2599 +```rust +fn from_json(mut json: serde_json::Value) -> Result +where + Self: TryIntoModel<::Model>, + <::Entity as EntityTrait>::Model: IntoActiveModel, + for<'de> <::Entity as EntityTrait>::Model: + serde::de::Deserialize<'de> + serde::Serialize, +``` +* `DerivePartialModel` now implement `FromQueryResult` by default, so there may be a potential conflict. Remove `FromQueryResult` in these cases https://github.com/SeaQL/sea-orm/pull/2653 +```rust +error[E0119]: conflicting implementations of trait `sea_orm::FromQueryResult` for type `CakeWithFruit` + | +> | #[derive(DerivePartialModel, FromQueryResult)] + | ------------------ ^^^^^^^^^^^^^^^ conflicting implementation for `CakeWithFruit` +``` +* Changed `IdenStatic` and `EntityName` definition https://github.com/SeaQL/sea-orm/pull/2667 +```rust +trait IdenStatic { + fn as_str(&self) -> &'static str; // added static lifetime +} +trait EntityName { + fn table_name(&self) -> &'static str; // added static lifetime +} +``` +* Removed `DeriveCustomColumn` and `default_as_str` https://github.com/SeaQL/sea-orm/pull/2667 +```rust +// This is no longer supported: +#[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)] +pub enum Column { + Id, + Name, +} + +impl IdenStatic for Column { + fn as_str(&self) -> &str { + match self { + Self::Name => "my_name", + _ => self.default_as_str(), + } + } +} + +// Do the following instead: +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + #[sea_orm(column_name = "my_name")] + Name, +} +``` +* `execute`, `query_one`, `query_all`, `stream` now takes in SeaQuery statement instead of raw SQL statement. a new set of methods `execute_raw`, `query_one_raw`, `query_all_raw`, `stream_raw` is added https://github.com/SeaQL/sea-orm/pull/2657 +```rust + --> src/executor/paginator.rs:53:38 + | +> | let rows = self.db.query_all(stmt).await?; + | --------- ^^^^ expected `&_`, found `Statement` + | | + | arguments to this method are incorrect + | + = note: expected reference `&_` + found struct `statement::Statement` +``` +```rust +let backend = self.db.get_database_backend(); +let stmt = backend.build(&query); +// change to: +let rows = self.db.query_all_raw(stmt).await?; +// if the query is a SeaQuery statement, then just do this: +let rows = self.db.query_all(&query).await?; // no need to build query +``` +* `DatabaseConnection` is changed from enum to struct. The original enum is moved into `DatabaseConnection::inner`. The new enum is named `DatabaseConnectionType` https://github.com/SeaQL/sea-orm/pull/2671 +```rust +error[E0599]: no associated item named `Disconnected` found for struct `db_connection::DatabaseConnection` in the current scope + --> src/database/db_connection.rs:137:33 + | +> | pub struct DatabaseConnection { + | ----------------------------- associated item `Disconnected` not found for this struct +... +> | DatabaseConnection::Disconnected => Err(conn_err("Disconnected")), + | ^^^^^^^^^^^^ associated item not found in `DatabaseConnection` +``` +```rust +match conn.inner { + DatabaseConnectionType::Disconnected => (), + _ => (), +} +``` +* `DeleteOne` and `UpdateOne` no longer implement `QueryFilter` and `QueryTrait` + directly. Those implementations could expose an incomplete SQL query with an + incomplete condition that touches too many records. To generate the right + condition, we must make sure that the primary key is set on the input + `ActiveModel`. If you need to access the generated SQL query, convert into + `ValidatedDeleteOne`/`ValidatedUpdateOne` first. +```rust +error[E0599]: no method named `build` found for struct `query::update::UpdateOne` in the current scope + --> src/entity/column.rs:607:22 + | + > | / Update::one(active_model) + > | | .build(DbBackend::Postgres) + | | -^^^^^ method not found in `UpdateOne` + | |_____________________| + | +``` +Call the `validate()` method: +```rust +Update::one(active_model) + + .validate()? + .build(DbBackend::Postgres) +``` +* Removed `DbBackend::get_query_builder()` because `QueryBuilder` is not longer object safe. +```rust + - fn get_query_builder(&self) -> Box +``` +* A number of methods has been removed from `SelectTwoMany`: `into_partial_model`, `into_json`, `stream`. These methods are same as those in `SelectTwo`. +Please use `Cake::find().find_also_related(Fruit).into_json()` instead. +* The `delete_by_id` method has changed to returning `DeleteOne` instead of `DeleteMany`. It doesn't change normal `exec` usage, but would change return type of `exec_with_returning` to `Option` +```rust +fn delete_by_id(values: T) -> DeleteMany // old + +fn delete_by_id(values: T) -> ValidatedDeleteOne // new +``` +* `DeriveActiveEnum` now also automatically impl `IntoActiveValue`, if you have a custom impl before, there would be a collision +* `with-bigdecimal` is now removed from default features +* `RuntimeErr::SqlxError` is now held in `Arc` to make `DbErr` clonable and smaller: +```rust +pub enum RuntimeErr { + SqlxError(Arc), +``` + +### Upgrades + +* Upgraded Rust Edition to 2024 https://github.com/SeaQL/sea-orm/pull/2596 +* Upgraded `strum` to `0.27` + +## 1.1.19 - 2025-11-11 + +### Enhancements + +* Add `find_linked_recursive` method to ModelTrait https://github.com/SeaQL/sea-orm/pull/2480 +* Skip drop extension type in fresh https://github.com/SeaQL/sea-orm/pull/2716 + +### Bug Fixes + +* Handle null values in `from_sqlx_*_row_to_proxy_row` functions https://github.com/SeaQL/sea-orm/pull/2744 + +## 1.1.17 - 2025-10-09 + +### New Features + +* Added `map_sqlx_mysql_opts`, `map_sqlx_postgres_opts`, `map_sqlx_sqlite_opts` to `ConnectOptions` https://github.com/SeaQL/sea-orm/pull/2731 +```rust +let mut opt = ConnectOptions::new(url); +opt.map_sqlx_postgres_opts(|pg_opt: PgConnectOptions| { + pg_opt.ssl_mode(PgSslMode::Require) +}); +``` +* Added `mariadb-use-returning` to use returning syntax for MariaDB https://github.com/SeaQL/sea-orm/pull/2710 +* Released `sea-orm-rocket` 0.6 https://github.com/SeaQL/sea-orm/pull/2732 + +## 1.1.16 - 2025-09-11 + +### Bug Fixes + +* Fix enum casting in DerivePartialModel https://github.com/SeaQL/sea-orm/pull/2719 https://github.com/SeaQL/sea-orm/pull/2720 +```rust +#[derive(DerivePartialModel)] +#[sea_orm(entity = "active_enum::Entity", from_query_result, alias = "zzz")] +struct PartialWithEnumAndAlias { + #[sea_orm(from_col = "tea")] + foo: Option, +} + +let sql = active_enum::Entity::find() + .into_partial_model::() + .into_statement(DbBackend::Postgres) + .sql; + +assert_eq!( + sql, + r#"SELECT CAST("zzz"."tea" AS "text") AS "foo" FROM "public"."active_enum""#, +); +``` + +### Enhancements + +* [sea-orm-cli] Use tokio (optional) instead of async-std https://github.com/SeaQL/sea-orm/pull/2721 + +## 1.1.15 - 2025-08-31 + +### Enhancements + +* Allow `DerivePartialModel` to have nested aliases https://github.com/SeaQL/sea-orm/pull/2686 +```rust +#[derive(DerivePartialModel)] +#[sea_orm(entity = "bakery::Entity", from_query_result)] +struct Factory { + id: i32, + #[sea_orm(from_col = "name")] + plant: String, +} + +#[derive(DerivePartialModel)] +#[sea_orm(entity = "cake::Entity", from_query_result)] +struct CakeFactory { + id: i32, + name: String, + #[sea_orm(nested, alias = "factory")] // <- new + bakery: Option, +} +``` +* Add `ActiveModelTrait::try_set` https://github.com/SeaQL/sea-orm/pull/2706 +```rust +fn set(&mut self, c: ::Column, v: Value); +/// New: a non-panicking version of above +fn try_set(&mut self, c: ::Column, v: Value) -> Result<(), DbErr>; +``` + +### Bug Fixes + +* [sea-orm-cli] Fix compilation issue https://github.com/SeaQL/sea-orm/pull/2713 + +## 1.1.14 - 2025-07-21 + +### Enhancements + +* [sea-orm-cli] Mask sensitive ENV values https://github.com/SeaQL/sea-orm/pull/2658 + +### Bug Fixes + +* `FromJsonQueryResult`: panic on serialization failures https://github.com/SeaQL/sea-orm/pull/2635 +```rust +#[derive(Clone, Debug, PartialEq, Deserialize, FromJsonQueryResult)] +pub struct NonSerializableStruct; + +impl Serialize for NonSerializableStruct { + fn serialize(&self, _serializer: S) -> Result + where + S: Serializer, + { + Err(serde::ser::Error::custom( + "intentionally failing serialization", + )) + } +} + +let model = Model { + json: Some(NonSerializableStruct), +}; + +let _ = model.into_active_model().insert(&ctx.db).await; // panic here +``` + +## 1.1.13 - 2025-06-29 + +### New Features + +* [sea-orm-cli] New `--frontend-format` flag to generate entities in pure Rust https://github.com/SeaQL/sea-orm/pull/2631 +```rust +// for example, below is the normal (compact) Entity: +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: i32, + #[sea_orm(column_type = "Text", nullable)] + pub name: Option , +} +// this is the generated frontend model, there is no SeaORM dependency: +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Model { + #[serde(skip_deserializing)] + pub id: i32, + pub name: Option , +} +``` + +### Enhancements + +* Removed potential panics from `Loader` https://github.com/SeaQL/sea-orm/pull/2637 + +## 1.1.12 - 2025-05-27 + +### Enhancements + +* Make sea-orm-cli & sea-orm-migration dependencies optional https://github.com/SeaQL/sea-orm/pull/2367 +* Relax TransactionError's trait bound for errors to allow `anyhow::Error` https://github.com/SeaQL/sea-orm/pull/2602 + +### Bug Fixes + +* Include custom `column_name` in DeriveColumn `Column::from_str` impl https://github.com/SeaQL/sea-orm/pull/2603 +```rust +#[derive(DeriveEntityModel)] +pub struct Model { + #[sea_orm(column_name = "lAsTnAmE")] + last_name: String, +} + +assert!(matches!(Column::from_str("lAsTnAmE").unwrap(), Column::LastName)); +``` + +## 1.1.11 - 2025-05-07 + +### Enhancements + +* Added `ActiveModelTrait::default_values` +```rust +assert_eq!( + fruit::ActiveModel::default_values(), + fruit::ActiveModel { + id: Set(0), + name: Set("".into()), + cake_id: Set(None), + type_without_default: NotSet, + }, +); +``` +* Impl `IntoCondition` for `RelationDef` https://github.com/SeaQL/sea-orm/pull/2587 +```rust +// This allows using `RelationDef` directly where sea-query expects an `IntoCondition` +let query = Query::select() + .from(fruit::Entity) + .inner_join(cake::Entity, fruit::Relation::Cake.def()) + .to_owned(); +``` +* Loader: retain only unique key values in the query condition https://github.com/SeaQL/sea-orm/pull/2569 +* Add proxy transaction impl https://github.com/SeaQL/sea-orm/pull/2573 +* [sea-orm-cli] Fix `PgVector` codegen https://github.com/SeaQL/sea-orm/pull/2589 + +### Bug fixes + +* Quote type properly in `AsEnum` casting https://github.com/SeaQL/sea-orm/pull/2570 +```rust +assert_eq!( + lunch_set::Entity::find() + .select_only() + .column(lunch_set::Column::Tea) + .build(DbBackend::Postgres) + .to_string(), + r#"SELECT CAST("lunch_set"."tea" AS "text") FROM "lunch_set""# + // "text" is now quoted; will work for "text"[] as well +); +``` +* Fix unicode string enum https://github.com/SeaQL/sea-orm/pull/2218 + +### Upgrades + +* Upgrade `heck` to `0.5` https://github.com/SeaQL/sea-orm/pull/2218 +* Upgrade `sea-query` to `0.32.5` +* Upgrade `sea-schema` to `0.16.2` + +## 1.1.10 - 2025-04-14 + +### Upgrades + +* Upgrade sqlx to 0.8.4 https://github.com/SeaQL/sea-orm/pull/2562 + +## 1.1.9 - 2025-04-14 + +### Enhancements + +* [sea-orm-macros] Use fully-qualified syntax for ActiveEnum associated type https://github.com/SeaQL/sea-orm/pull/2552 +* Accept `LikeExpr` in `like` and `not_like` https://github.com/SeaQL/sea-orm/pull/2549 + +### Bug fixes + +* Check if url is well-formed before parsing https://github.com/SeaQL/sea-orm/pull/2558 +* `QuerySelect::column_as` method cast ActiveEnum column https://github.com/SeaQL/sea-orm/pull/2551 + +### House keeping + +* Remove redundant `Expr::expr` from internal code https://github.com/SeaQL/sea-orm/pull/2554 + +## 1.1.8 - 2025-03-30 + +### New Features + +* Implement `DeriveValueType` for enum strings +```rust +#[derive(DeriveValueType)] +#[sea_orm(value_type = "String")] +pub enum Tag { + Hard, + Soft, +} + +// `from_str` defaults to `std::str::FromStr::from_str` +impl std::str::FromStr for Tag { + type Err = sea_orm::sea_query::ValueTypeErr; + fn from_str(s: &str) -> Result { .. } +} + +// `to_str` defaults to `std::string::ToString::to_string`. +impl std::fmt::Display for Tag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { .. } +} + +// you can override from_str and to_str with custom functions +#[derive(DeriveValueType)] +#[sea_orm(value_type = "String", from_str = "Tag::from_str", to_str = "Tag::to_str")] +pub enum Tag { + Color, + Grey, +} + +impl Tag { + fn from_str(s: &str) -> Result { .. } + + fn to_str(&self) -> &'static str { .. } +} +``` +* Support Postgres Ipnetwork (under feature flag `with-ipnetwork`) https://github.com/SeaQL/sea-orm/pull/2395 +```rust +// Model +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "host_network")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub ipaddress: IpNetwork, + #[sea_orm(column_type = "Cidr")] + pub network: IpNetwork, +} + +// Schema +sea_query::Table::create() + .table(host_network::Entity) + .col(ColumnDef::new(host_network::Column::Id).integer().not_null().auto_increment().primary_key()) + .col(ColumnDef::new(host_network::Column::Ipaddress).inet().not_null()) + .col(ColumnDef::new(host_network::Column::Network).cidr().not_null()) + .to_owned(); + +// CRUD +host_network::ActiveModel { + ipaddress: Set(IpNetwork::new(Ipv6Addr::new(..))), + network: Set(IpNetwork::new(Ipv4Addr::new(..))), + ..Default::default() +} +``` + +### Enhancements + +* Added `try_getable_postgres_array!(Vec)` (to support `bytea[]`) https://github.com/SeaQL/sea-orm/pull/2503 + +### Bug fixes + +* [sea-orm-codegen] Support postgres array in expanded format https://github.com/SeaQL/sea-orm/pull/2545 + +### House keeping + +* Replace `once_cell` crate with `std` equivalent https://github.com/SeaQL/sea-orm/pull/2524 +(available since rust 1.80) + +## 1.1.7 - 2025-03-02 + +### New Features + +* Support nested entities in `FromQueryResult` https://github.com/SeaQL/sea-orm/pull/2508 +```rust +#[derive(FromQueryResult)] +struct Cake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, +} + +#[derive(FromQueryResult)] +struct CakeBakery { + #[sea_orm(from_alias = "bakery_id")] + id: i32, + #[sea_orm(from_alias = "bakery_name")] + title: String, +} + +let cake: Cake = cake::Entity::find() + .select_only() + .column(cake::Column::Id) + .column(cake::Column::Name) + .column_as(bakery::Column::Id, "bakery_id") + .column_as(bakery::Column::Name, "bakery_name") + .left_join(bakery::Entity) + .order_by_asc(cake::Column::Id) + .into_model() + .one(&ctx.db) + .await? + .unwrap(); + +assert_eq!( + cake, + Cake { + id: 1, + name: "Cake".to_string(), + bakery: Some(CakeBakery { + id: 20, + title: "Bakery".to_string(), + }) + } +); +``` +* Support nested entities in `DerivePartialModel` https://github.com/SeaQL/sea-orm/pull/2508 +```rust +#[derive(DerivePartialModel)] // FromQueryResult is no longer needed +#[sea_orm(entity = "cake::Entity", from_query_result)] +struct Cake { + id: i32, + name: String, + #[sea_orm(nested)] + bakery: Option, +} + +#[derive(DerivePartialModel)] +#[sea_orm(entity = "bakery::Entity", from_query_result)] +struct Bakery { + id: i32, + #[sea_orm(from_col = "Name")] + title: String, +} + +// same as previous example, but without the custom selects +let cake: Cake = cake::Entity::find() + .left_join(bakery::Entity) + .order_by_asc(cake::Column::Id) + .into_partial_model() + .one(&ctx.db) + .await? + .unwrap(); + +assert_eq!( + cake, + Cake { + id: 1, + name: "Cake".to_string(), + bakery: Some(CakeBakery { + id: 20, + title: "Bakery".to_string(), + }) + } +); +``` +* Derive also `IntoActiveModel` with `DerivePartialModel` https://github.com/SeaQL/sea-orm/pull/2517 +```rust +#[derive(DerivePartialModel)] +#[sea_orm(entity = "cake::Entity", into_active_model)] +struct Cake { + id: i32, + name: String, +} + +assert_eq!( + Cake { + id: 12, + name: "Lemon Drizzle".to_owned(), + } + .into_active_model(), + cake::ActiveModel { + id: Set(12), + name: Set("Lemon Drizzle".to_owned()), + ..Default::default() + } +); +``` +* Added `SelectThree` https://github.com/SeaQL/sea-orm/pull/2518 +```rust +// Order -> (many) Lineitem -> Cake +let items: Vec<(order::Model, Option, Option)> = + order::Entity::find() + .find_also_related(lineitem::Entity) + .and_also_related(cake::Entity) + .order_by_asc(order::Column::Id) + .order_by_asc(lineitem::Column::Id) + .all(&ctx.db) + .await?; +``` + +### Enhancements + +* Support complex type path in `DeriveIntoActiveModel` https://github.com/SeaQL/sea-orm/pull/2517 +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "::ActiveModel")] +struct Fruit { + cake_id: Option>, +} +``` +* Added `DatabaseConnection::close_by_ref` https://github.com/SeaQL/sea-orm/pull/2511 +```rust +pub async fn close(self) -> Result<(), DbErr> { .. } // existing +pub async fn close_by_ref(&self) -> Result<(), DbErr> { .. } // new +``` + +### House Keeping + +* Cleanup legacy `ActiveValue::Set` https://github.com/SeaQL/sea-orm/pull/2515 + +## 1.1.6 - 2025-02-24 + +### New Features + +* Support PgVector (under feature flag `postgres-vector`) https://github.com/SeaQL/sea-orm/pull/2500 +```rust +// Model +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "image_model")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i32, + pub embedding: PgVector, +} + +// Schema +sea_query::Table::create() + .table(image_model::Entity.table_ref()) + .col(ColumnDef::new(Column::Id).integer().not_null().primary_key()) + .col(ColumnDef::new(Column::Embedding).vector(None).not_null()) + .. + +// Insert +ActiveModel { + id: NotSet, + embedding: Set(PgVector::from(vec![1., 2., 3.])), +} +.insert(db) +.await? +``` +* Added `Insert::exec_with_returning_keys` & `Insert::exec_with_returning_many` (Postgres only) +```rust +assert_eq!( + Entity::insert_many([ + ActiveModel { id: NotSet, name: Set("two".into()) }, + ActiveModel { id: NotSet, name: Set("three".into()) }, + ]) + .exec_with_returning_many(db) + .await + .unwrap(), + [ + Model { id: 2, name: "two".into() }, + Model { id: 3, name: "three".into() }, + ] +); + +assert_eq!( + cakes_bakers::Entity::insert_many([ + cakes_bakers::ActiveModel { + cake_id: Set(1), + baker_id: Set(2), + }, + cakes_bakers::ActiveModel { + cake_id: Set(2), + baker_id: Set(1), + }, + ]) + .exec_with_returning_keys(db) + .await + .unwrap(), + [(1, 2), (2, 1)] +); +``` +* Added `DeleteOne::exec_with_returning` & `DeleteMany::exec_with_returning` https://github.com/SeaQL/sea-orm/pull/2432 + +### Enhancements + +* Expose underlying row types (e.g. `sqlx::postgres::PgRow`) https://github.com/SeaQL/sea-orm/pull/2265 +* [sea-orm-cli] Added `acquire-timeout` option https://github.com/SeaQL/sea-orm/pull/2461 +* [sea-orm-cli] Added `with-prelude` option https://github.com/SeaQL/sea-orm/pull/2322 +* [sea-orm-cli] Added `impl-active-model-behavior` option https://github.com/SeaQL/sea-orm/pull/2487 + +### Bug Fixes + +* Fixed `seaography::register_active_enums` macro https://github.com/SeaQL/sea-orm/pull/2475 + +### House keeping + +* Remove `futures` crate, replace with `futures-util` https://github.com/SeaQL/sea-orm/pull/2466 + +## 1.1.5 - 2025-02-14 + +### New Features + +* Added `Schema::json_schema_from_entity` to construct a schema description in json for the given Entity + +## 1.1.4 - 2025-01-10 + +### Enhancements + +* Allow modifying the connection in migrations https://github.com/SeaQL/sea-orm/pull/2397 +* `DeriveRelatedEntity` proc_macro use `async-graphql` re-exported by `seaography` https://github.com/SeaQL/sea-orm/pull/2469 + +## 1.1.3 - 2024-12-24 + +### New Features + +* [sea-orm-codegen] register seaography entity modules & active enums https://github.com/SeaQL/sea-orm/pull/2403 +```rust +pub mod prelude; + +pub mod sea_orm_active_enums; + +pub mod baker; +pub mod bakery; +pub mod cake; +pub mod cakes_bakers; +pub mod customer; +pub mod lineitem; +pub mod order; + +seaography::register_entity_modules!([ + baker, + bakery, + cake, + cakes_bakers, + customer, + lineitem, + order, +]); + +seaography::register_active_enums!([ + sea_orm_active_enums::Tea, + sea_orm_active_enums::Color, +]); +``` + +### Enhancements + +* Insert many allow active models to have different column set https://github.com/SeaQL/sea-orm/pull/2433 +```rust +// this previously panics +let apple = cake_filling::ActiveModel { + cake_id: ActiveValue::set(2), + filling_id: ActiveValue::NotSet, +}; +let orange = cake_filling::ActiveModel { + cake_id: ActiveValue::NotSet, + filling_id: ActiveValue::set(3), +}; +assert_eq!( + Insert::::new() + .add_many([apple, orange]) + .build(DbBackend::Postgres) + .to_string(), + r#"INSERT INTO "cake_filling" ("cake_id", "filling_id") VALUES (2, NULL), (NULL, 3)"#, +); +``` +* [sea-orm-cli] Added `MIGRATION_DIR` environment variable https://github.com/SeaQL/sea-orm/pull/2419 +* Added `ColumnDef::is_unique` https://github.com/SeaQL/sea-orm/pull/2401 +* Postgres: quote schema in `search_path` https://github.com/SeaQL/sea-orm/pull/2436 + +### Bug Fixes + +* MySQL: fix transaction isolation level not respected when used with access mode https://github.com/SeaQL/sea-orm/pull/2450 + +## 1.1.2 - 2024-12-02 + +### Enhancements + +* Added `ColumnTrait::enum_type_name()` to signify enum types https://github.com/SeaQL/sea-orm/pull/2415 +* Added `DbBackend::boolean_value()` for database dependent boolean value https://github.com/SeaQL/sea-orm/pull/2415 + +## 1.1.1 - 2024-11-04 + +### Enhancements + +* [sea-orm-macros] `impl From for ActiveModel` instead of `impl From<::Model> for ActiveModel` https://github.com/SeaQL/sea-orm/pull/2349. +Now the following can compile: +```rust +use sea_orm::{tests_cfg::cake, Set}; + +struct Cake { + id: i32, + name: String, +} + +impl From for cake::ActiveModel { + fn from(value: Cake) -> Self { + Self { + id: Set(value.id), + name: Set(value.name), + } + } +} +``` + +## 1.1.0 - 2024-10-15 + +### Versions + ++ `1.1.0-rc.1`: 2024-08-09 ++ `1.1.0-rc.2`: 2024-10-04 ++ `1.1.0-rc.3`: 2024-10-08 + +### Enhancements + +* [sea-orm-macros] Call `EnumIter::get` using fully qualified syntax https://github.com/SeaQL/sea-orm/pull/2321 +* Construct `DatabaseConnection` directly from `sqlx::PgPool`, `sqlx::SqlitePool` and `sqlx::MySqlPool` https://github.com/SeaQL/sea-orm/pull/2348 +* [sea-orm-migration] Add `pk_uuid` schema helper https://github.com/SeaQL/sea-orm/pull/2329 +* [sea-orm-migration] Allow `custom` and `custom_null` schema helper to take column name and alias of different `IntoIden` types https://github.com/SeaQL/sea-orm/pull/2326 +* Add `ColumnDef::get_column_default` getter https://github.com/SeaQL/sea-orm/pull/2387 + +### Upgrades + +* Upgrade `sqlx` to `0.8.2` https://github.com/SeaQL/sea-orm/pull/2305, https://github.com/SeaQL/sea-orm/pull/2371 +* Upgrade `bigdecimal` to `0.4` https://github.com/SeaQL/sea-orm/pull/2305 +* Upgrade `sea-query` to `0.32.0-rc` https://github.com/SeaQL/sea-orm/pull/2305 +* Upgrade `sea-query-binder` to `0.7.0-rc` https://github.com/SeaQL/sea-orm/pull/2305 +* Upgrade `sea-schema` to `0.16.0-rc` https://github.com/SeaQL/sea-orm/pull/2305 +* Upgrade `ouroboros` to `0.18` https://github.com/SeaQL/sea-orm/pull/2353 + +### House keeping + +* Fix typos https://github.com/SeaQL/sea-orm/pull/2360 +* Update documentations https://github.com/SeaQL/sea-orm/pull/2345 + +## 1.0.1 - 2024-08-26 + +### New Features + +* Added `ConnectOptions::connect_lazy` for creating DB connection pools without establishing connections up front https://github.com/SeaQL/sea-orm/pull/2268 + +### Breaking Changes + +* Changed `ProxyDatabaseTrait` methods to async. It's a breaking change, but it should have been part of the 1.0 release. + The feature is behind the feature guard `proxy`, and we believe it shouldn't impact majority of users. + https://github.com/SeaQL/sea-orm/pull/2278 + +### Bug Fixes + +* [sea-orm-codegen] Fix `ColumnType` to Rust type resolution https://github.com/SeaQL/sea-orm/pull/2313 + +## 1.0.0 - 2024-08-02 + +### Versions + ++ `1.0.0-rc.1`: 2024-02-06 ++ `1.0.0-rc.2`: 2024-03-15 ++ `1.0.0-rc.3`: 2024-03-26 ++ `1.0.0-rc.4`: 2024-05-13 ++ `1.0.0-rc.5`: 2024-05-29 ++ `1.0.0-rc.6`: 2024-06-19 ++ `1.0.0-rc.7`: 2024-06-25 + +### New Features + +* Introduce `PrimaryKeyArity` with `ARITY` constant https://github.com/SeaQL/sea-orm/pull/2185 +```rust +fn get_arity_of() -> usize { + E::PrimaryKey::iter().count() // before; runtime + <::ValueType as PrimaryKeyArity>::ARITY // now; compile-time +} +``` +* Associate `ActiveModel` to `EntityTrait` https://github.com/SeaQL/sea-orm/pull/2186 +* [sea-orm-macros] Added `rename_all` attribute to `DeriveEntityModel` & `DeriveActiveEnum` https://github.com/SeaQL/sea-orm/pull/2170 +```rust +#[derive(DeriveEntityModel)] +#[sea_orm(table_name = "user", rename_all = "camelCase")] +pub struct Model { + #[sea_orm(primary_key)] + id: i32, + first_name: String, // firstName + #[sea_orm(column_name = "lAsTnAmE")] + last_name: String, // lAsTnAmE +} + +#[derive(EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "camelCase")] +pub enum TestEnum { + DefaultVariant, // defaultVariant + #[sea_orm(rename = "kebab-case")] + VariantKebabCase, // variant-kebab-case + #[sea_orm(rename = "snake_case")] + VariantSnakeCase, // variant_snake_case + #[sea_orm(string_value = "CuStOmStRiNgVaLuE")] + CustomStringValue, // CuStOmStRiNgVaLuE +} +``` +* [sea-orm-migration] schema helper https://github.com/SeaQL/sea-orm/pull/2099 +```rust +// Remember to import `sea_orm_migration::schema::*` +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Users::Table) + .if_not_exists() + .col(pk_auto(Users::Id)) // Primary key with auto-increment + .col(uuid(Users::Pid)) // UUID column + .col(string_uniq(Users::Email)) // String column with unique constraint + .col(string(Users::Password)) // String column + .col(string(Users::ApiKey).unique_key()) + .col(string(Users::Name)) + .col(string_null(Users::ResetToken)) // Nullable string column + .col(timestamp_null(Users::ResetSentAt)) // Nullable timestamp column + .col(string_null(Users::EmailVerificationToken)) + .col(timestamp_null(Users::EmailVerificationSentAt)) + .col(timestamp_null(Users::EmailVerifiedAt)) + .to_owned(), + ) + .await + } + + // ... +} +``` + +### Enhancements + +* Added non-TLS runtime https://github.com/SeaQL/sea-orm/pull/2256 +* Added `QuerySelect::tbl_col_as` +* Added `Insert::on_conflict_do_nothing` https://github.com/SeaQL/sea-orm/pull/2244 +* Migration schema nullable column set NULL explicitly https://github.com/SeaQL/sea-orm/pull/2255 +* Added `ActiveValue::set_if_not_equals()` https://github.com/SeaQL/sea-orm/pull/2194 +* Added `ActiveValue::try_as_ref()` https://github.com/SeaQL/sea-orm/pull/2197 +* Added `QuerySelect::order_by_with_nulls` https://github.com/SeaQL/sea-orm/pull/2228 +* Expose `get_xxx_connection_pool` by default https://github.com/SeaQL/sea-orm/pull/2233 +* Added `QueryResult::column_names` https://github.com/SeaQL/sea-orm/pull/2148 +* [sea-orm-macro] Add `@generated` in generated code https://github.com/SeaQL/sea-orm/pull/2199 +* [sea-orm-macro] Qualify traits in `DeriveActiveModel` macro https://github.com/SeaQL/sea-orm/pull/1665 +* [sea-orm-cli] Fix `migrate generate` on empty `mod.rs` files https://github.com/SeaQL/sea-orm/pull/2064 +* `DerivePartialModel` macro attribute `entity` now supports `syn::Type` https://github.com/SeaQL/sea-orm/pull/2137 +```rust +#[derive(DerivePartialModel)] +#[sea_orm(entity = "::Entity")] +struct EntityNameNotAIdent { + #[sea_orm(from_col = "foo2")] + _foo: i32, + #[sea_orm(from_col = "bar2")] + _bar: String, +} +``` +* Added `RelationDef::from_alias()` https://github.com/SeaQL/sea-orm/pull/2146 +```rust +let cf = Alias::new("cf"); + +assert_eq!( + cake::Entity::find() + .join_as( + JoinType::LeftJoin, + cake_filling::Relation::Cake.def().rev(), + cf.clone() + ) + .join( + JoinType::LeftJoin, + cake_filling::Relation::Filling.def().from_alias(cf) + ) + .build(DbBackend::MySql) + .to_string(), + [ + "SELECT `cake`.`id`, `cake`.`name` FROM `cake`", + "LEFT JOIN `cake_filling` AS `cf` ON `cake`.`id` = `cf`.`cake_id`", + "LEFT JOIN `filling` ON `cf`.`filling_id` = `filling`.`id`", + ] + .join(" ") +); +``` + +### Bug Fixes + +* Set schema search path in Postgres without enclosing single quote https://github.com/SeaQL/sea-orm/pull/2241 +* [sea-orm-cli] Generate `has_one` relation for foreign key of unique index / constraint https://github.com/SeaQL/sea-orm/pull/2254 + +### Breaking changes + +* Renamed `ConnectOptions::pool_options()` to `ConnectOptions::sqlx_pool_options()` https://github.com/SeaQL/sea-orm/pull/2145 +* Made `sqlx_common` private, hiding `sqlx_error_to_xxx_err` https://github.com/SeaQL/sea-orm/pull/2145 +* Rework SQLite type mappings https://github.com/SeaQL/sea-orm/pull/2077, https://github.com/SeaQL/sea-orm/pull/2078 + +### Upgrades + +* Upgrade `time` to `0.3.36` https://github.com/SeaQL/sea-orm/pull/2267 +* Upgrade `strum` to `0.26` https://github.com/SeaQL/sea-orm/pull/2088 +* Upgrade `sea-schema` to `0.15.0` +* Upgrade `sea-query-binder` to `0.6.0` +* Upgrade `sea-query` to `0.31.0` + +### House keeping + +* Reduce warnings in integration tests https://github.com/SeaQL/sea-orm/pull/2177 +* Improved Actix example to return 404 not found on unexpected inputs https://github.com/SeaQL/sea-orm/pull/2140 +* Re-enable `rocket_okapi` example https://github.com/SeaQL/sea-orm/pull/2136 + +## 1.0.0-rc.7 - 2024-06-25 + +### Upgrades + +* Upgrade `sea-query-binder` to `0.6.0-rc.4` https://github.com/SeaQL/sea-orm/pull/2267 +* Upgrade `time` to `0.3.36` https://github.com/SeaQL/sea-orm/pull/2267 + +## 1.0.0-rc.6 - 2024-06-19 + +### Enhancements + +* Added non-TLS runtime https://github.com/SeaQL/sea-orm/pull/2256 +* Added `QuerySelect::tbl_col_as` +* Added `Insert::on_conflict_do_nothing` https://github.com/SeaQL/sea-orm/pull/2244 +* Migration schema nullable column set NULL explicitly https://github.com/SeaQL/sea-orm/pull/2255 + +### Bug Fixes + +* Set schema search path in Postgres without enclosing single quote https://github.com/SeaQL/sea-orm/pull/2241 +* [sea-orm-cli] Generate `has_one` relation for foreign key of unique index / constraint https://github.com/SeaQL/sea-orm/pull/2254 + +## 1.0.0-rc.5 - 2024-05-29 + +### New Features + +* Introduce `PrimaryKeyArity` with `ARITY` constant https://github.com/SeaQL/sea-orm/pull/2185 +```rust +fn get_arity_of() -> usize { + E::PrimaryKey::iter().count() // before; runtime + <::ValueType as PrimaryKeyArity>::ARITY // now; compile-time +} +``` +* Associate `ActiveModel` to `EntityTrait` https://github.com/SeaQL/sea-orm/pull/2186 +* [sea-orm-macros] Added `rename_all` attribute to `DeriveEntityModel` & `DeriveActiveEnum` https://github.com/SeaQL/sea-orm/pull/2170 +```rust +#[derive(DeriveEntityModel)] +#[sea_orm(table_name = "user", rename_all = "camelCase")] +pub struct Model { + #[sea_orm(primary_key)] + id: i32, + first_name: String, // firstName + #[sea_orm(column_name = "lAsTnAmE")] + last_name: String, // lAsTnAmE +} + +#[derive(EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "camelCase")] +pub enum TestEnum { + DefaultVariant, // defaultVariant + #[sea_orm(rename = "kebab-case")] + VariantKebabCase, // variant-kebab-case + #[sea_orm(rename = "snake_case")] + VariantSnakeCase, // variant_snake_case + #[sea_orm(string_value = "CuStOmStRiNgVaLuE")] + CustomStringValue, // CuStOmStRiNgVaLuE +} +``` + +### Enhancements + +* Added `ActiveValue::set_if_not_equals()` https://github.com/SeaQL/sea-orm/pull/2194 +* Added `ActiveValue::try_as_ref()` https://github.com/SeaQL/sea-orm/pull/2197 +* Added `QuerySelect::order_by_with_nulls` https://github.com/SeaQL/sea-orm/pull/2228 +* Expose `get_xxx_connection_pool` by default https://github.com/SeaQL/sea-orm/pull/2233 + +## 1.0.0-rc.4 - 2024-05-13 + +### Enhancements + +* Added `QueryResult::column_names` https://github.com/SeaQL/sea-orm/pull/2148 +* [sea-orm-macro] Add `@generated` in generated code https://github.com/SeaQL/sea-orm/pull/2199 + +### Upgrades + +* Upgrade `sea-query` to `0.31.0-rc.6` +* Upgrade `sea-schema` to `0.15.0-rc.6` + +### House Keeping + +* Reduce warnings in integration tests https://github.com/SeaQL/sea-orm/pull/2177 + +## 1.0.0-rc.3 - 2024-03-26 + +### Enhancements + +* [sea-orm-macro] Qualify traits in `DeriveActiveModel` macro https://github.com/SeaQL/sea-orm/pull/1665 + +## 1.0.0-rc.2 - 2024-03-15 + +### Breaking Changes + +* Renamed `ConnectOptions::pool_options()` to `ConnectOptions::sqlx_pool_options()` https://github.com/SeaQL/sea-orm/pull/2145 +* Made `sqlx_common` private, hiding `sqlx_error_to_xxx_err` https://github.com/SeaQL/sea-orm/pull/2145 + +### Enhancements + +* [sea-orm-cli] Fix `migrate generate` on empty `mod.rs` files https://github.com/SeaQL/sea-orm/pull/2064 +* `DerivePartialModel` macro attribute `entity` now supports `syn::Type` https://github.com/SeaQL/sea-orm/pull/2137 +```rust +#[derive(DerivePartialModel)] +#[sea_orm(entity = "::Entity")] +struct EntityNameNotAIdent { + #[sea_orm(from_col = "foo2")] + _foo: i32, + #[sea_orm(from_col = "bar2")] + _bar: String, +} +``` +* Added `RelationDef::from_alias()` https://github.com/SeaQL/sea-orm/pull/2146 +```rust +let cf = Alias::new("cf"); + +assert_eq!( + cake::Entity::find() + .join_as( + JoinType::LeftJoin, + cake_filling::Relation::Cake.def().rev(), + cf.clone() + ) + .join( + JoinType::LeftJoin, + cake_filling::Relation::Filling.def().from_alias(cf) + ) + .build(DbBackend::MySql) + .to_string(), + [ + "SELECT `cake`.`id`, `cake`.`name` FROM `cake`", + "LEFT JOIN `cake_filling` AS `cf` ON `cake`.`id` = `cf`.`cake_id`", + "LEFT JOIN `filling` ON `cf`.`filling_id` = `filling`.`id`", + ] + .join(" ") +); +``` + +### Upgrades + +* Upgrade `sea-schema` to `0.15.0-rc.3` +* Upgrade `strum` to `0.26` https://github.com/SeaQL/sea-orm/pull/2088 + +### House keeping + +* Improved Actix example to return 404 not found on unexpected inputs https://github.com/SeaQL/sea-orm/pull/2140 +* Re-enable `rocket_okapi` example https://github.com/SeaQL/sea-orm/pull/2136 + +## 1.0.0-rc.1 - 2024-02-06 ### New Features -* Supports for partial select of `Option` model field. A `None` value will be filled when the select result does not contain the `Option` field without throwing an error. https://github.com/SeaQL/sea-orm/pull/1513 -```rs -customer::ActiveModel { - name: Set("Alice".to_owned()), - notes: Set(Some("Want to communicate with Bob".to_owned())), - ..Default::default() +* [sea-orm-migration] schema helper https://github.com/SeaQL/sea-orm/pull/2099 +```rust +// Remember to import `sea_orm_migration::schema::*` +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Users::Table) + .if_not_exists() + .col(pk_auto(Users::Id)) // Primary key with auto-increment + .col(uuid(Users::Pid)) // UUID column + .col(string_uniq(Users::Email)) // String column with unique constraint + .col(string(Users::Password)) // String column + .col(string(Users::ApiKey).unique_key()) + .col(string(Users::Name)) + .col(string_null(Users::ResetToken)) // Nullable string column + .col(timestamp_null(Users::ResetSentAt)) // Nullable timestamp column + .col(string_null(Users::EmailVerificationToken)) + .col(timestamp_null(Users::EmailVerificationSentAt)) + .col(timestamp_null(Users::EmailVerifiedAt)) + .to_owned(), + ) + .await + } + + // ... } -.save(db) -.await?; +``` -// The `notes` field was intentionally leaved out -let customer = Customer::find() - .select_only() - .column(customer::Column::Id) - .column(customer::Column::Name) - .one(db) - .await - .unwrap(); +### Breaking Changes + +* Rework SQLite type mappings https://github.com/SeaQL/sea-orm/pull/2077, https://github.com/SeaQL/sea-orm/pull/2078 +* Updated `sea-query` to `0.31` + +## 0.12.14 - 2024-02-05 + +* Added feature flag `sqlite-use-returning-for-3_35` to use SQLite's returning https://github.com/SeaQL/sea-orm/pull/2070 +* Added Loco example https://github.com/SeaQL/sea-orm/pull/2092 + +## 0.12.12 - 2024-01-22 + +### Bug Fixes + +* [sea-orm-cli] Fix entity generation for non-alphanumeric enum variants https://github.com/SeaQL/sea-orm/pull/1821 +* [sea-orm-cli] Fix entity generation for relations with composite keys https://github.com/SeaQL/sea-orm/pull/2071 + +### Enhancements + +* Added `ConnectOptions::test_before_acquire` + +## 0.12.11 - 2024-01-14 + +### New Features + +* Added `desc` to `Cursor` paginator https://github.com/SeaQL/sea-orm/pull/2037 + +### Enhancements + +* Improve query performance of `Paginator`'s `COUNT` query https://github.com/SeaQL/sea-orm/pull/2030 +* Added SQLx slow statements logging to `ConnectOptions` https://github.com/SeaQL/sea-orm/pull/2055 +* Added `QuerySelect::lock_with_behavior` https://github.com/SeaQL/sea-orm/pull/1867 + +### Bug Fixes + +* [sea-orm-macro] Qualify types in `DeriveValueType` macro https://github.com/SeaQL/sea-orm/pull/2054 + +### House keeping + +* Fix clippy warnings on 1.75 https://github.com/SeaQL/sea-orm/pull/2057 + +## 0.12.10 - 2023-12-14 + +### New Features + +* [sea-orm-macro] Comment attribute for Entity (`#[sea_orm(comment = "action")]`); `create_table_from_entity` supports comment https://github.com/SeaQL/sea-orm/pull/2009 +* Added "proxy" (feature flag `proxy`) to database backend https://github.com/SeaQL/sea-orm/pull/1881, https://github.com/SeaQL/sea-orm/pull/2000 + +### Enhancements + +* Cast enums in `is_in` and `is_not_in` https://github.com/SeaQL/sea-orm/pull/2002 + +### Upgrades + +* Updated `sea-query` to `0.30.5` https://github.com/SeaQL/sea-query/releases/tag/0.30.5 + +## 0.12.9 - 2023-12-08 + +### Enhancements + +* Add source annotations to errors https://github.com/SeaQL/sea-orm/pull/1999 + +### Upgrades + +* Updated `sea-query` to `0.30.4` https://github.com/SeaQL/sea-query/releases/tag/0.30.4 + +## 0.12.8 - 2023-12-04 + +### Enhancements + +* Implement `StatementBuilder` for `sea_query::WithQuery` https://github.com/SeaQL/sea-orm/issues/1960 + +### Upgrades -// The select result does not contain `notes` field. -// Since it's of type `Option`, it'll be `None` and no error will be thrown. -assert_eq!(customers.notes, None); +* Upgrade `axum` example to `0.7` https://github.com/SeaQL/sea-orm/pull/1984 + +## 0.12.7 - 2023-11-22 + +### Enhancements + +* Added method `expr_as_` that accepts `self` https://github.com/SeaQL/sea-orm/pull/1979 + +### Upgrades + +* Updated `sea-query` to `0.30.3` https://github.com/SeaQL/sea-query/releases/tag/0.30.3 + +## 0.12.6 - 2023-11-13 + +### New Features + +* Added `#[sea_orm(skip)]` for `FromQueryResult` derive macro https://github.com/SeaQL/sea-orm/pull/1954 + +## 0.12.5 - 2023-11-12 + +### Bug Fixes + +* [sea-orm-cli] Fix duplicated active enum use statements on generated entities https://github.com/SeaQL/sea-orm/pull/1953 +* [sea-orm-cli] Added `--enum-extra-derives` https://github.com/SeaQL/sea-orm/pull/1934 +* [sea-orm-cli] Added `--enum-extra-attributes` https://github.com/SeaQL/sea-orm/pull/1952 + +## 0.12.4 - 2023-10-19 + +### New Features + +* Add support for root JSON arrays https://github.com/SeaQL/sea-orm/pull/1898 + Now the following works (requires the `json-array` / `postgres-array` feature)! +```rust +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "json_struct_vec")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Json")] + pub struct_vec: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct JsonColumn { + pub value: String, +} ``` -* [sea-orm-cli] the `migrate init` command will create a `.gitignore` file when the migration folder reside in a Git repository https://github.com/SeaQL/sea-orm/pull/1334 + +### Enhancements + +* Loader: use `ValueTuple` as hash key https://github.com/SeaQL/sea-orm/pull/1868 + +### Upgrades + +* Updated `sea-query` to `0.30.2` https://github.com/SeaQL/sea-query/releases/tag/0.30.2 + +## 0.12.3 - 2023-09-22 + +### New Features + +* [sea-orm-migration] Check if an index exists https://github.com/SeaQL/sea-orm/pull/1828 +* Added `cursor_by` to `SelectTwo` https://github.com/SeaQL/sea-orm/pull/1826 + +### Enhancements + +* [sea-orm-cli] Support generation of related entity with composite foreign key https://github.com/SeaQL/sea-orm/pull/1693 + +### Bug Fixes + +* [sea-orm-macro] Fixed `DeriveValueType` by qualifying `QueryResult` https://github.com/SeaQL/sea-orm/pull/1855 +* Fixed `Loader` panic on empty inputs + +### Upgrades + +* Upgraded `salvo` to `0.50` +* Upgraded `chrono` to `0.4.30` https://github.com/SeaQL/sea-orm/pull/1858 +* Updated `sea-query` to `0.30.1` +* Updated `sea-schema` to `0.14.1` + +### House keeping + +* Added test cases for `find_xxx_related/linked` https://github.com/SeaQL/sea-orm/pull/1811 + +## 0.12.2 - 2023-08-04 + +### Enhancements + +* Added support for Postgres arrays in `FromQueryResult` impl of `JsonValue` https://github.com/SeaQL/sea-orm/pull/1598 + +### Bug fixes + +* Fixed `find_with_related` consolidation logic https://github.com/SeaQL/sea-orm/issues/1800 + +## 0.12.1 - 2023-07-27 + ++ `0.12.0-rc.1`: Yanked ++ `0.12.0-rc.2`: 2023-05-19 ++ `0.12.0-rc.3`: 2023-06-22 ++ `0.12.0-rc.4`: 2023-07-08 ++ `0.12.0-rc.5`: 2023-07-22 + +### New Features + * Added `MigratorTrait::migration_table_name()` method to configure the name of migration table https://github.com/SeaQL/sea-orm/pull/1511 -```rs +```rust #[async_trait::async_trait] impl MigratorTrait for Migrator { - fn migrations() -> Vec> { - vec![ - Box::new(m20220118_000001_create_cake_table::Migration), - Box::new(m20220118_000002_create_fruit_table::Migration), - ] - } - // Override the name of migration table fn migration_table_name() -> sea_orm::DynIden { Alias::new("override_migration_table_name").into_iden() } + ... } ``` * Added option to construct chained AND / OR join on condition https://github.com/SeaQL/sea-orm/pull/1433 -```rs -use sea_orm::entity::prelude::*; - +```rust #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, - #[sea_orm(column_name = "name", enum_name = "Name")] pub name: String, } @@ -83,90 +2133,22 @@ pub enum Relation { )] OrTropicalFruit, } - -impl ActiveModelBehavior for ActiveModel {} -``` -You can also override it in custom join. -```rs -assert_eq!( - cake::Entity::find() - .column_as( - Expr::col((Alias::new("cake_filling_alias"), cake_filling::Column::CakeId)), - "cake_filling_cake_id" - ) - .join(JoinType::LeftJoin, cake::Relation::OrTropicalFruit.def()) - .join_as_rev( - JoinType::LeftJoin, - cake_filling::Relation::Cake - .def() - // chained AND / OR join on condition - .condition_type(ConditionType::Any) - .on_condition(|left, _right| { - Expr::col((left, cake_filling::Column::CakeId)) - .gt(10) - .into_condition() - }), - Alias::new("cake_filling_alias") - ) - .build(DbBackend::MySql) - .to_string(), - [ - "SELECT `cake`.`id`, `cake`.`name`, `cake_filling_alias`.`cake_id` AS `cake_filling_cake_id` FROM `cake`", - "LEFT JOIN `fruit` ON `cake`.`id` = `fruit`.`cake_id` OR `fruit`.`name` LIKE '%tropical%'", - "LEFT JOIN `cake_filling` AS `cake_filling_alias` ON `cake_filling_alias`.`cake_id` = `cake`.`id` OR `cake_filling_alias`.`cake_id` > 10", - ] - .join(" ") -); ``` -* Supports entity with composite primary key of length 12 https://github.com/SeaQL/sea-orm/pull/1508 - * Implemented `IntoIdentity` for `Identity` https://github.com/SeaQL/sea-orm/pull/1508 - * `Identity` supports up to identity tuple of `DynIden` with length up to 12 https://github.com/SeaQL/sea-orm/pull/1508 - * Implemented `IntoIdentity` for tuple of `IdenStatic` with length up to 12 https://github.com/SeaQL/sea-orm/pull/1508 - * Implemented `IdentityOf` for tuple of `ColumnTrait` with length up to 12 https://github.com/SeaQL/sea-orm/pull/1508 - * Implemented `TryGetableMany` for tuple of `TryGetable` with length up to 12 https://github.com/SeaQL/sea-orm/pull/1508 - * Implemented `TryFromU64` for tuple of `TryFromU64` with length up to 12 https://github.com/SeaQL/sea-orm/pull/1508 -```rs -use sea_orm::entity::prelude::*; - +* Supports entity with composite primary key of arity 12 https://github.com/SeaQL/sea-orm/pull/1508 + * `Identity` supports tuple of `DynIden` with arity up to 12 +```rust #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "primary_key_of_12")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id_1: String, - #[sea_orm(primary_key, auto_increment = false)] - pub id_2: i8, - #[sea_orm(primary_key, auto_increment = false)] - pub id_3: u8, - #[sea_orm(primary_key, auto_increment = false)] - pub id_4: i16, - #[sea_orm(primary_key, auto_increment = false)] - pub id_5: u16, - #[sea_orm(primary_key, auto_increment = false)] - pub id_6: i32, - #[sea_orm(primary_key, auto_increment = false)] - pub id_7: u32, - #[sea_orm(primary_key, auto_increment = false)] - pub id_8: i64, - #[sea_orm(primary_key, auto_increment = false)] - pub id_9: u64, - #[sea_orm(primary_key, auto_increment = false)] - pub id_10: f32, - #[sea_orm(primary_key, auto_increment = false)] - pub id_11: f64, + ... #[sea_orm(primary_key, auto_increment = false)] pub id_12: bool, - pub owner: String, - pub name: String, - pub description: String, } - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} ``` * Added macro `DerivePartialModel` https://github.com/SeaQL/sea-orm/pull/1597 -```rs +```rust #[derive(DerivePartialModel, FromQueryResult)] #[sea_orm(entity = "Cake")] struct PartialCake { @@ -185,39 +2167,119 @@ assert_eq!( r#"SELECT "cake"."name", UPPER("cake"."name") AS "name_upper" FROM "cake""# ); ``` -* [sea-orm-cli] Added support for generating migration of space separated name, for example executing `sea-orm-cli migrate generate "create accounts table"` command will create `m20230503_000000_create_accounts_table.rs` for you https://github.com/SeaQL/sea-orm/pull/1570 +* Added `DbErr::sql_err()` method to convert error into common database errors `SqlErr`, such as unique constraint or foreign key violation errors. https://github.com/SeaQL/sea-orm/pull/1707 +```rust +assert!(matches!( + cake.into_active_model().insert(db).await + .expect_err("Insert a row with duplicated primary key") + .sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) +)); + +assert!(matches!( + fk_cake.insert(db).await + .expect_err("Insert a row with invalid foreign key") + .sql_err(), + Some(SqlErr::ForeignKeyConstraintViolation(_)) +)); +``` +* Added `Select::find_with_linked`, similar to `find_with_related`: https://github.com/SeaQL/sea-orm/pull/1728, https://github.com/SeaQL/sea-orm/pull/1743 +```rust +fn find_with_related(self, r: R) -> SelectTwoMany + where R: EntityTrait, E: Related; +fn find_with_linked(self, l: L) -> SelectTwoMany + where L: Linked, T: EntityTrait; -* Add `seaography` flag to `sea-orm`, `sea-orm-orm-macros` and `sea-orm-cli` https://github.com/SeaQL/sea-orm/pull/1599 -* Add generation of `seaography` related information to `sea-orm-codegen` https://github.com/SeaQL/sea-orm/pull/1599 +// boths yields `Vec<(E::Model, Vec)>` +``` +* Added `DeriveValueType` derive macro for custom wrapper types, implementations of the required traits will be provided, you can customize the `column_type` and `array_type` if needed https://github.com/SeaQL/sea-orm/pull/1720 +```rust +#[derive(DeriveValueType)] +#[sea_orm(array_type = "Int")] +pub struct Integer(i32); + +#[derive(DeriveValueType)] +#[sea_orm(column_type = "Boolean", array_type = "Bool")] +pub struct Boolbean(pub String); + +#[derive(DeriveValueType)] +pub struct StringVec(pub Vec); +``` +* Added `DeriveDisplay` derive macro to implements `std::fmt::Display` for enum https://github.com/SeaQL/sea-orm/pull/1726 +```rust +#[derive(DeriveDisplay)] +enum DisplayTea { + EverydayTea, + #[sea_orm(display_value = "Breakfast Tea")] + BreakfastTea, +} +assert_eq!(format!("{}", DisplayTea::EverydayTea), "EverydayTea"); +assert_eq!(format!("{}", DisplayTea::BreakfastTea), "Breakfast Tea"); +``` +* Added `UpdateMany::exec_with_returning()` https://github.com/SeaQL/sea-orm/pull/1677 +```rust +let models: Vec = Entity::update_many() + .col_expr(Column::Values, Expr::expr(..)) + .exec_with_returning(db) + .await?; +``` +* Supporting `default_expr` in `DeriveEntityModel` https://github.com/SeaQL/sea-orm/pull/1474 +```rust +#[derive(DeriveEntityModel)] +#[sea_orm(table_name = "hello")] +pub struct Model { + #[sea_orm(default_expr = "Expr::current_timestamp()")] + pub timestamp: DateTimeUtc, +} - The following information is added in entities files by `sea-orm-cli` when flag `seaography` is `true` +assert_eq!( + Column::Timestamp.def(), + ColumnType::TimestampWithTimeZone.def() + .default(Expr::current_timestamp()) +); +``` +* Introduced new `ConnAcquireErr` https://github.com/SeaQL/sea-orm/pull/1737 ```rust -/// ... Entity File ... +enum DbErr { + ConnectionAcquire(ConnAcquireErr), + .. +} + +enum ConnAcquireErr { + Timeout, + ConnectionClosed, +} +``` + +#### Seaography +Added Seaography integration https://github.com/SeaQL/sea-orm/pull/1599 + +* Added `DeriveEntityRelated` macro which will implement `seaography::RelationBuilder` for `RelatedEntity` enumeration when the `seaography` feature is enabled +* Added generation of `seaography` related information to `sea-orm-codegen`. + + The `RelatedEntity` enum is added in entities files by `sea-orm-cli` when flag `seaography` is set: +```rust +/// SeaORM Entity #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { - #[sea_orm(entity = "super::address::Entity")] - Address, - #[sea_orm(entity = "super::payment::Entity")] - Payment, - #[sea_orm(entity = "super::rental::Entity")] - Rental, - #[sea_orm(entity = "Entity", def = "Relation::SelfRef.def()")] - SelfRef, - #[sea_orm(entity = "super::store::Entity")] - Store, - #[sea_orm(entity = "Entity", def = "Relation::SelfRef.def().rev()")] - SelfRefRev, + #[sea_orm(entity = "super::bakery::Entity")] + Bakery, + #[sea_orm(entity = "super::cake_baker::Entity")] + CakeBaker, + #[sea_orm(entity = "super::cake::Entity")] + Cake, } ``` -* Add `DeriveEntityRelated` macro https://github.com/SeaQL/sea-orm/pull/1599 - - The DeriveRelatedEntity derive macro will implement `seaography::RelationBuilder` for `RelatedEntity` enumeration when the `seaography` feature is enabled +* Added [`seaography_example`](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) ### Enhancements +* Supports for partial select of `Option` model field. A `None` value will be filled when the select result does not contain the `Option` field without throwing an error. https://github.com/SeaQL/sea-orm/pull/1513 +* [sea-orm-cli] the `migrate init` command will create a `.gitignore` file when the migration folder reside in a Git repository https://github.com/SeaQL/sea-orm/pull/1334 +* [sea-orm-cli] Added support for generating migration of space separated name, for example executing `sea-orm-cli migrate generate "create accounts table"` command will create `m20230503_000000_create_accounts_table.rs` for you https://github.com/SeaQL/sea-orm/pull/1570 * Added `Migration::name()` and `Migration::status()` getters for the name and status of `sea_orm_migration::Migration` https://github.com/SeaQL/sea-orm/pull/1519 -```rs +```rust let migrations = Migrator::get_pending_migrations(db).await?; assert_eq!(migrations.len(), 5); @@ -235,87 +2297,63 @@ assert_eq!(migration.status(), MigrationStatus::Pending); * Changed the parameter of method `Transaction::from_sql_and_values(DbBackend, T, I) where I: IntoIterator, T: Into` to takes any string SQL * Changed the parameter of method `ConnectOptions::set_schema_search_path(T) where T: Into` to takes any string * Changed the parameter of method `ColumnTrait::like()`, `ColumnTrait::not_like()`, `ColumnTrait::starts_with()`, `ColumnTrait::ends_with()` and `ColumnTrait::contains()` to takes any string -* Re-export `sea_query::{DynIden, RcOrArc, SeaRc}` in `sea_orm::entity::prelude` module https://github.com/SeaQL/sea-orm/pull/1661 +* Added `sea_query::{DynIden, RcOrArc, SeaRc}` to entity prelude https://github.com/SeaQL/sea-orm/pull/1661 +* Added `expr`, `exprs` and `expr_as` methods to `QuerySelect` trait https://github.com/SeaQL/sea-orm/pull/1702 +* Added `DatabaseConnection::ping` https://github.com/SeaQL/sea-orm/pull/1627 +```rust +|db: DatabaseConnection| { + assert!(db.ping().await.is_ok()); + db.clone().close().await; + assert!(matches!(db.ping().await, Err(DbErr::ConnectionAcquire))); +} +``` +* Added `TryInsert` that does not panic on empty inserts https://github.com/SeaQL/sea-orm/pull/1708 +```rust +// now, you can do: +let res = Bakery::insert_many(std::iter::empty()) + .on_empty_do_nothing() + .exec(db) + .await; -### Upgrades +assert!(matches!(res, Ok(TryInsertResult::Empty))); +``` +* Insert on conflict do nothing to return Ok https://github.com/SeaQL/sea-orm/pull/1712 +```rust +let on = OnConflict::column(Column::Id).do_nothing().to_owned(); -* Upgrade `heck` dependency in `sea-orm-macros` and `sea-orm-codegen` to 0.4 https://github.com/SeaQL/sea-orm/pull/1520, https://github.com/SeaQL/sea-orm/pull/1544 -* Upgrade `strum` to 0.24 -* Upgrade `sea-query` to `0.29` https://github.com/SeaQL/sea-orm/pull/1562 -* Upgrade `sea-query-binder` to `0.4` https://github.com/SeaQL/sea-orm/pull/1562 -* Upgrade `sea-schema` to `0.12` https://github.com/SeaQL/sea-orm/pull/1562 +// Existing behaviour +let res = Entity::insert_many([..]).on_conflict(on).exec(db).await; +assert!(matches!(res, Err(DbErr::RecordNotInserted))); + +// New API; now you can: +let res = +Entity::insert_many([..]).on_conflict(on).do_nothing().exec(db).await; +assert!(matches!(res, Ok(TryInsertResult::Conflicted))); +``` ### Bug Fixes * Fixed `DeriveActiveEnum` throwing errors because `string_value` consists non-UAX#31 compliant characters https://github.com/SeaQL/sea-orm/pull/1374 - -For example, ```rust -#[derive(Clone, Debug, PartialEq, EnumIter, DeriveActiveEnum)] +#[derive(EnumIter, DeriveActiveEnum)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum StringValue { #[sea_orm(string_value = "")] Member1, - #[sea_orm(string_value = "$")] - Member2, #[sea_orm(string_value = "$$")] - Member3, - #[sea_orm(string_value = "AB")] - Member4, - #[sea_orm(string_value = "A_B")] - Member5, - #[sea_orm(string_value = "A$B")] - Member6, - #[sea_orm(string_value = "0 123")] - Member7, + Member2, } -``` -will now produce the following Variant Enum: -```rust +// will now produce the following enum: pub enum StringValueVariant { __Empty, - _0x24, _0x240x24, - Ab, - A0x5Fb, - A0x24B, - _0x300x20123, -} -``` -* [sea-orm-cli] The implementation of `Related` with `via` and `to` methods will not be generated if there exists multiple paths via an intermediate table. Like in the schema defined below - Path 1. `users <-> users_votes <-> bills`, Path 2. `users <-> users_saved_bills <-> bills` https://github.com/SeaQL/sea-orm/pull/1435 -```sql -CREATE TABLE users -( - id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(), - email TEXT UNIQUE NOT NULL, - ... -); -``` -```sql -CREATE TABLE bills -( - id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(), - ... -); -``` -```sql -CREATE TABLE users_votes -( - user_id uuid REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE, - bill_id uuid REFERENCES bills (id) ON UPDATE CASCADE ON DELETE CASCADE, - vote boolean NOT NULL, - CONSTRAINT users_bills_pkey PRIMARY KEY (user_id, bill_id) -); -``` -```sql -CREATE TABLE users_saved_bills -( - user_id uuid REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE, - bill_id uuid REFERENCES bills (id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT users_saved_bills_pkey PRIMARY KEY (user_id, bill_id) -); +} ``` +* [sea-orm-cli] Fix Postgres enum arrays https://github.com/SeaQL/sea-orm/pull/1678 +* [sea-orm-cli] The implementation of `Related` with `via` and `to` methods will not be generated if there exists multiple paths via an intermediate table https://github.com/SeaQL/sea-orm/pull/1435 * [sea-orm-cli] fixed entity generation includes partitioned tables https://github.com/SeaQL/sea-orm/issues/1582, https://github.com/SeaQL/sea-schema/pull/105 +* Fixed `ActiveEnum::db_type()` return type does not implement `ColumnTypeTrait` https://github.com/SeaQL/sea-orm/pull/1576 +* Resolved `insert_many` failing if the models iterator is empty https://github.com/SeaQL/sea-orm/issues/873 ### Breaking changes @@ -324,7 +2362,55 @@ CREATE TABLE users_saved_bills * Added `derive` and `strum` features to `sea-orm-macros` * The derive macro `EnumIter` is now shipped by `sea-orm-macros` * Added a new variant `Many` to `Identity` https://github.com/SeaQL/sea-orm/pull/1508 -* Replace the use of `SeaRc` where `T` isn't `dyn Iden` with `RcOrArc` https://github.com/SeaQL/sea-orm/pull/1661 +* Enabled `hashable-value` feature in SeaQuery, thus `Value::Float(NaN) == Value::Float(NaN)` would be true https://github.com/SeaQL/sea-orm/pull/1728, https://github.com/SeaQL/sea-orm/pull/1743 +* The `DeriveActiveEnum` derive macro no longer implement `std::fmt::Display`. You can use the new `DeriveDisplay` macro https://github.com/SeaQL/sea-orm/pull/1726 +* `sea-query/derive` is no longer enabled by `sea-orm`, as such, `Iden` no longer works as a derive macro (it's still a trait). Instead, we are shipping a new macro `DeriveIden` https://github.com/SeaQL/sea-orm/pull/1740 https://github.com/SeaQL/sea-orm/pull/1755 +```rust +// then: + +#[derive(Iden)] +#[iden = "category"] +pub struct CategoryEnum; + +#[derive(Iden)] +pub enum Tea { + Table, + #[iden = "EverydayTea"] + EverydayTea, +} + +// now: + +#[derive(DeriveIden)] +#[sea_orm(iden = "category")] +pub struct CategoryEnum; + +#[derive(DeriveIden)] +pub enum Tea { + Table, + #[sea_orm(iden = "EverydayTea")] + EverydayTea, +} +``` +* Definition of `DbErr::ConnectionAcquire` changed to `ConnectionAcquire(ConnAcquireErr)` https://github.com/SeaQL/sea-orm/pull/1737 +* `FromJsonQueryResult` removed from entity prelude + +### Upgrades + +* Upgraded `sqlx` to `0.7` https://github.com/SeaQL/sea-orm/pull/1742 +* Upgraded `sea-query` to `0.30` https://github.com/SeaQL/sea-orm/pull/1742 +* Upgraded `sea-schema` to `0.14` https://github.com/SeaQL/sea-orm/pull/1742 +* Upgraded `syn` to `2` https://github.com/SeaQL/sea-orm/pull/1713 +* Upgraded `heck` to `0.4` https://github.com/SeaQL/sea-orm/pull/1520, https://github.com/SeaQL/sea-orm/pull/1544 +* Upgraded `strum` to `0.25` https://github.com/SeaQL/sea-orm/pull/1752 +* Upgraded `clap` to `4.3` https://github.com/SeaQL/sea-orm/pull/1468 +* Upgraded `ouroboros` to `0.17` https://github.com/SeaQL/sea-orm/pull/1724 + +### House keeping + +* Replaced `bae` with `sea-bae` https://github.com/SeaQL/sea-orm/pull/1739 + +**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.11.1...0.12.1 ## 0.11.3 - 2023-04-24 @@ -332,14 +2418,14 @@ CREATE TABLE users_saved_bills * Re-export `sea_orm::ConnectionTrait` in `sea_orm_migration::prelude` https://github.com/SeaQL/sea-orm/pull/1577 * Support generic structs in `FromQueryResult` derive macro https://github.com/SeaQL/sea-orm/pull/1464, https://github.com/SeaQL/sea-orm/pull/1603 -```rs +```rust #[derive(FromQueryResult)] struct GenericTest { foo: i32, bar: T, } ``` -```rs +```rust trait MyTrait { type Item: TryGetable; } @@ -370,7 +2456,7 @@ where * Fixes `DeriveActiveEnum` (by qualifying `ColumnTypeTrait::def`) https://github.com/SeaQL/sea-orm/issues/1478 * The CLI command `sea-orm-cli generate entity -u ''` will now generate the following code for each `Binary` or `VarBinary` columns in compact format https://github.com/SeaQL/sea-orm/pull/1529 -```rs +```rust #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "binary")] pub struct Model { @@ -391,7 +2477,7 @@ pub struct Model { } ``` * The CLI command `sea-orm-cli generate entity -u '' --expanded-format` will now generate the following code for each `Binary` or `VarBinary` columns in expanded format https://github.com/SeaQL/sea-orm/pull/1529 -```rs +```rust impl ColumnTrait for Column { type EntityName = Entity; fn def(&self) -> ColumnDef { @@ -574,7 +2660,7 @@ impl ColumnTrait for Column { * Fixed a small typo https://github.com/SeaQL/sea-orm/pull/1391 * `axum` example should use tokio runtime https://github.com/SeaQL/sea-orm/pull/1428 -Full Changelog: https://github.com/SeaQL/sea-orm/compare/0.10.0...0.11.0 +**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.10.0...0.11.0 ## 0.10.7 - 2023-01-19 @@ -1086,8 +3172,6 @@ In this minor release, we removed `time` v0.1 from the dependency graph * Temporary Fix: Handling MySQL & SQLite timestamp columns by @billy1624 in https://github.com/SeaQL/sea-orm/pull/379 * Add feature to generate table Iden by @Sytten in https://github.com/SeaQL/sea-orm/pull/360 -**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.4.1...0.4.2 - ## 0.4.1 - 2021-12-05 ### Fixed Issues @@ -1101,8 +3185,6 @@ In this minor release, we removed `time` v0.1 from the dependency graph * Support Up to 6 Values Composite Primary Key by @billy1624 in https://github.com/SeaQL/sea-orm/pull/353 * Codegen Handle Self Referencing & Multiple Relations to the Same Related Entity by @billy1624 in https://github.com/SeaQL/sea-orm/pull/347 -**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.4.0...0.4.1 - ## 0.4.0 - 2021-11-19 ### Fixed Issues @@ -1209,8 +3291,6 @@ In this minor release, we removed `time` v0.1 from the dependency graph * Documentation for sea-orm by @charleschege in https://github.com/SeaQL/sea-orm/pull/280 * Support `Vec` primary key by @billy1624 in https://github.com/SeaQL/sea-orm/pull/287 -**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.3.1...0.3.2 - ## 0.3.1 - 2021-10-23 (We are changing our Changelog format from now on) @@ -1228,8 +3308,6 @@ In this minor release, we removed `time` v0.1 from the dependency graph * CI cleaning by @AngelOnFira in https://github.com/SeaQL/sea-orm/pull/263 * CI install sea-orm-cli in debug mode by @billy1624 in https://github.com/SeaQL/sea-orm/pull/265 -**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.3.0...0.3.1 - ## 0.3.0 - 2021-10-15 https://www.sea-ql.org/SeaORM/blog/2021-10-15-whats-new-in-0.3.0 @@ -1289,6 +3367,8 @@ assert_eq!( [#237]: https://github.com/SeaQL/sea-orm/pull/237 [#246]: https://github.com/SeaQL/sea-orm/pull/246 +**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.2.6...0.3.0 + ## 0.2.6 - 2021-10-09 - [[#224]] [sea-orm-cli] Date & Time column type mapping @@ -1368,6 +3448,8 @@ https://www.sea-ql.org/SeaORM/blog/2021-10-01-whats-new-in-0.2.4 [#89]: https://github.com/SeaQL/sea-orm/issues/89 [#59]: https://github.com/SeaQL/sea-orm/issues/59 +**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.1.3...0.2.0 + ## 0.1.3 - 2021-08-30 - [[#108]] Remove impl TryGetable for Option diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..75d0848c6f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,220 @@ +# SeaORM Project Guidelines + +This project uses **SeaORM 2.0**. AI models likely have SeaORM 1.0 in their training data -- some patterns have changed. Always follow the 2.0 patterns shown below. + +## Quick Reference Links + +- [Walk-through of SeaORM 2.0](https://www.sea-ql.org/blog/2025-12-05-sea-orm-2.0/) +- [Migration Guide (1.0 to 2.0)](https://www.sea-ql.org/blog/2026-01-12-sea-orm-2.0/) +- [New Entity Format](https://www.sea-ql.org/blog/2025-10-20-sea-orm-2.0/) +- [Strongly-Typed Column](https://www.sea-ql.org/blog/2025-11-11-sea-orm-2.0/) +- [Nested ActiveModel](https://www.sea-ql.org/blog/2025-11-25-sea-orm-2.0/) +- [Entity First Workflow](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/) + +## Entity Definition (2.0 Format) + +In SeaORM 2.0, entities use `#[sea_orm::model]` with relations defined directly on the `Model` struct. This replaces the 1.0 pattern of separate `Relation` enums and `Related` trait impls. + +```rust +mod user { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "user")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub email: String, + #[sea_orm(has_one)] + pub profile: HasOne, + #[sea_orm(has_many)] + pub posts: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} +``` + +### Relation Attributes + +```rust +// Has-One +#[sea_orm(has_one)] +pub profile: HasOne, + +// Has-Many +#[sea_orm(has_many)] +pub posts: HasMany, + +// Belongs-To (explicit foreign key mapping) +#[sea_orm(belongs_to, from = "user_id", to = "id")] +pub user: HasOne, + +// Many-to-Many via junction table +#[sea_orm(has_many, via = "post_tag")] +pub tags: HasMany, + +// Self-referential +#[sea_orm(self_ref, via = "user_follower", from = "User", to = "Follower")] +pub followers: HasMany, +``` + +### Junction Table (Composite Primary Key) + +```rust +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "post_tag")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub post_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub tag_id: i32, + #[sea_orm(belongs_to, from = "post_id", to = "id")] + pub post: Option, + #[sea_orm(belongs_to, from = "tag_id", to = "id")] + pub tag: Option, +} +``` + +## Strongly-Typed Columns (2.0) + +Use `COLUMN` constant with typed fields instead of the untyped `Column` enum: + +```rust +// 2.0 (preferred) -- compile-time type safety +user::Entity::find().filter(user::COLUMN.name.contains("Bob")) + +// 1.0 (outdated) -- still works but prefer COLUMN +user::Entity::find().filter(user::Column::Name.contains("Bob")) +``` + +## ActiveModel Builder Pattern (2.0) + +```rust +// Create with nested relations +let bob = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("Tennis")) + .insert(db) + .await?; + +// Add has-many children +let mut bob = bob.into_active_model(); +bob.posts.push( + post::ActiveModel::builder().set_title("My first post") +); +bob.save(db).await?; + +// Many-to-many +let post = post::ActiveModel::builder() + .set_title("A sunny day") + .set_user_id(bob.id) + .add_tag(existing_tag) + .add_tag(tag::ActiveModel::builder().set_tag("outdoor")) + .save(db) + .await?; +``` + +## Entity Loader API (2.0) + +```rust +// Load with relations in a single query +let bob = user::Entity::load() + .filter_by_email("bob@sea-ql.org") + .with(profile::Entity) + .with(post::Entity) + .one(db) + .await? + .expect("Not found"); + +// Nested relations (post -> comments) +let user = user::Entity::load() + .filter_by_id(12) + .with(profile::Entity) + .with((post::Entity, comment::Entity)) + .one(db) + .await?; +``` + +## Schema Registry (Entity-First Workflow) + +```rust +// Auto-create tables from entity definitions (dev/testing) +db.get_schema_registry("my_crate::*") + .sync(db) + .await?; +``` + +## Anti-Patterns -- DO NOT DO THESE + +### 1. Do not specify `column_type` on custom wrapper types + +When using `DeriveValueType` for custom types, the column type is inferred automatically from the inner type. Adding `column_type` is redundant and incorrect: + +```rust +// WRONG -- do not annotate column_type on custom types +#[sea_orm(column_type = "Decimal(Some((10, 4)))")] +pub speed: Speed, + +// CORRECT -- SeaORM infers the column type from the DeriveValueType inner type +pub speed: Speed, + +#[derive(Clone, Debug, PartialEq, DeriveValueType)] +pub struct Speed(Decimal); +``` + +### 2. Use `Text` or explicit max length for long strings on MySQL/MSSQL + +On MySQL and MSSQL, `String` maps to `VARCHAR(255)` by default. For strings that may exceed 255 characters, use `Text` or specify `StringLen::Max`: + +```rust +// WRONG on MySQL/MSSQL -- silently truncates at 255 chars +pub description: String, + +// CORRECT -- use column_type for longer strings +#[sea_orm(column_type = "Text")] +pub description: String, + +// Also correct -- explicit max length +#[sea_orm(column_type = "String(StringLen::Max)")] +pub event_type: String, +``` + +Note: Postgre / SQLite uses unbounded string by default, so this is primarily a MySQL/MSSQL concern. + +### 3. Missing `ExprTrait` import + +Methods like `.eq()`, `.like()`, `.contains()` on `Expr` require the trait import in 2.0: + +```rust +use sea_orm::ExprTrait; // required in 2.0 + +Expr::col((self.entity_name(), *self)).like(s) +``` + +### 4. Do not use removed or renamed APIs + +| 1.0 (removed/renamed) | 2.0 (correct) | +|---|---| +| `.into_condition()` | `.into()` | +| `db.execute(Statement::from_sql_and_values(..))` | `db.execute_raw(Statement::from_sql_and_values(..))` | +| `db.query_all(backend.build(&query))` | `db.query_all(&query)` | +| `Alias::new("col")` for static strings | `Expr::col("col")` directly | +| `insert_many(..).on_empty_do_nothing()` | `insert_many([])` returns `None` safely | + +### 5. Do not manually impl traits that `DeriveValueType` now generates + +In 2.0, `DeriveValueType` auto-generates `NotU8`, `IntoActiveValue`, and `TryFromU64`. Remove manual implementations to avoid conflicts. + +### 6. PostgreSQL: `serial` is no longer the default + +Auto-increment columns now use `GENERATED BY DEFAULT AS IDENTITY`. If you need legacy `serial` behavior, use feature flag `option-postgres-use-serial` or `.custom("serial")`. + +### 7. SQLite: integer type mapping changed + +Both `Integer` and `BigInteger` map to `integer` in 2.0. The entity generator produces `i64` by default. Override with `sea-orm-cli --big-integer-type=i32` if needed. diff --git a/COMMUNITY.md b/COMMUNITY.md index 394c7a9087..a832a18599 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -7,12 +7,16 @@ If you have built an app using SeaORM and want to showcase it, feel free to open ### Startups - [Caido](https://caido.io/) | A lightweight web security auditing toolkit -- [CodeCTRL](https://codectrl.authentura.com) ([repository](https://github.com/Authentura/codectrl)) | A self-hostable code logging platform | DB: SQLite +- [FirstLook.gg](https://firstlook.gg/) | A platform to onboard, understand, and reward players | DB: Postgres +- [Lapdev](https://lap.dev/) [![GitHub stars](https://img.shields.io/github/stars/lapce/lapdev.svg?style=social)](https://github.com/lapce/lapdev) | Self-hosted remote development enviroment | DB: Postgres +- [OpenObserve](https://openobserve.ai/) [![GitHub stars](https://img.shields.io/github/stars/openobserve/openobserve.svg?style=social)](https://github.com/openobserve/openobserve) | Open-source observability platform | DB: MySQL, Postgres, SQLite - [My Data My Consent](https://mydatamyconsent.com/) | Online data sharing for people and businesses simplified -- [Prefix.dev](https://prefix.dev/) | Conda Package Search, Environment Management and Deployment built for [mamba](https://github.com/mamba-org/mamba) | DB: Postgres, SQLite -- [Sensei](https://l2.technology/sensei) ([repository](https://github.com/L2-Technology/sensei)) | A Bitcoin lightning node implementation | DB: MySQL, Postgres, SQLite -- [Spyglass](https://www.spyglass.fyi/) ([repository](https://github.com/a5huynh/spyglass)) ![GitHub stars](https://img.shields.io/github/stars/a5huynh/spyglass.svg?style=social) | 🔭 A personal search engine that indexes what you want w/ a simple set of rules. | DB: SQLite -- [Svix](https://www.svix.com/) ([repository](https://github.com/svix/svix-webhooks)) ![GitHub stars](https://img.shields.io/github/stars/svix/svix-webhooks.svg?style=social) | The enterprise ready webhooks service | DB: Postgres +- [Prefix.dev](https://prefix.dev/) | Conda Package Search, Environment Management and Deployment built for mamba | DB: Postgres, SQLite +- [RisingWave](https://risingwave.com/) [![GitHub stars](https://img.shields.io/github/stars/risingwavelabs/risingwave.svg?style=social)](https://github.com/risingwavelabs/risingwave) | Stream processing and management platform | DB: MySQL, Postgres, SQLite +- [Svix](https://www.svix.com/) [![GitHub stars](https://img.shields.io/github/stars/svix/svix-webhooks.svg?style=social)](https://github.com/svix/svix-webhooks) | The enterprise ready webhooks service | DB: Postgres +- [System Initiative](https://www.systeminit.com/) [![GitHub stars](https://img.shields.io/github/stars/systeminit/si.svg?style=social)](https://github.com/systeminit/si) | DevOps Automation Platform | DB: Postgres +- [UpVPN](https://upvpn.app) [![GitHub stars](https://img.shields.io/github/stars/upvpn/upvpn-app.svg?style=social)](https://github.com/upvpn/upvpn-app) | Serverless VPN on WireGuard® | DB: SQLite, Postgres +- [Zed](https://zed.dev/) [![GitHub stars](https://img.shields.io/github/stars/zed-industries/zed.svg?style=social)](https://github.com/zed-industries/zed) | A high-performance, multiplayer code editor | DB: Postgres ### Open Source Projects @@ -21,10 +25,12 @@ If you have built an app using SeaORM and want to showcase it, feel free to open - [Backpack](https://github.com/JSH32/Backpack) | Open source self hosted file sharing platform on crack | DB: MySQL, Postgres, SQLite - [Dev Board](https://github.com/goto-eof/dev_board_api_rust) | A dashboard for organizing software development tasks implemented in Rust - [Iron Guard](https://github.com/AfaanBilal/iron-guard-server) | An inventory management system HTTP REST API server built with Rocket and SeaORM | DB: MySQL -- [mediarepo](https://mediarepo.trivernis.dev) ([repository](https://github.com/Trivernis/mediarepo)) ![GitHub stars](https://img.shields.io/github/stars/Trivernis/mediarepo.svg?style=social) | A tag-based media management application | DB: SQLite +- [mediarepo](https://mediarepo.trivernis.dev) ([repository](https://github.com/Trivernis/mediarepo)) | A tag-based media management application | DB: SQLite - [mugen](https://github.com/koopa1338/mugen-dms) | DMS written in 🦀 | DB: Postgres -- [OctoBase](https://github.com/toeverything/OctoBase) ![GitHub stars](https://img.shields.io/github/stars/toeverything/OctoBase.svg?style=social) | A light-weight, scalable, offline collaborative data backend written in 🦀 | DB: MySQL, Postgres, SQLite -- [Wikijump](https://github.com/scpwiki/wikijump) ([repository](https://github.com/scpwiki/wikijump/tree/develop/deepwell)) ![GitHub stars](https://img.shields.io/github/stars/scpwiki/wikijump.svg?style=social) | API service for Wikijump, a fork of Wikidot | DB: Postgres +- [OctoBase](https://github.com/toeverything/OctoBase) ![GitHub stars](https://img.shields.io/github/stars/toeverything/OctoBase.svg?style=social) | A light-weight, scalable, offline collaborative data backend | DB: MySQL, Postgres, SQLite +- [Ryot](https://ryot.io/) ([repository](https://github.com/IgnisDa/ryot)) ![GitHub stars](https://img.shields.io/github/stars/ignisda/ryot.svg?style=social) | The only self hosted tracker you will ever need | DB: MySQL, Postgres, SQLite +- [Wikijump](https://github.com/scpwiki/wikijump) ([repository](https://github.com/scpwiki/wikijump/tree/develop/deepwell)) | API service for Wikijump, a fork of Wikidot | DB: Postgres +- [sero](https://github.com/clowzed/sero): Host static sites with custom subdomains as surge.sh does. But with full control and cool new features. DB: Postgres #### Game - [Ceobe Canteen Serve](https://github.com/Enraged-Dun-Cookie-Development-Team/Ceobe-Canteen-Serve) | A tool based on Arknights mobile game, using axum as web framework | DB: MySQL @@ -35,41 +41,51 @@ If you have built an app using SeaORM and want to showcase it, feel free to open #### Social - [aeroFans](https://github.com/naryand/aerofans) | Full stack forum-like social media platform in Rust and WebAssembly | DB: Postgres +- [Crab Fit](https://crab.fit) | Align your schedules to find the perfect time that works for everyone. | DB: MySQL, Postgres, SQLite +- [Hatsu](https://github.com/importantimport/hatsu) | 🩵 Self-hosted & Fully-automated ActivityPub Bridge for Static Sites. | DB: SQLite, Postgres - [JinShu](https://github.com/gengteng/jinshu) | A cross-platform **I**nstant **M**essaging system written in 🦀 | DB: MySQL, Postgres -- [Kitsune](https://github.com/kitsune-soc/kitsune) | 🦊 ActivityPub-federated microblogging | DB: Postgres, SQLite +- [Portfolio](https://github.com/admisio/Portfolio) | Encrypted high school 🏫 admissions service | DB: Postgres, SQLite - [THUBurrow](https://github.com/BobAnkh/THUBurrow) | A campus forum built by Next.js and Rocket | DB: Postgres +- [playa](https://github.com/whizzes/playa) | 🏖️ Decentralized Social Platform powered by Rust and Whizzes Contributors #### Bots - [bulbbot-gw](https://github.com/TeamBulbbot/bulbbot-gw) | The gateway code for the Discord bot Bulbbot | DB: Postgres - [Fikabot](https://github.com/sousandrei/fikabot) | A slack bot to schedule coffee breaks (Fika in swedish) in slack channels | DB: MySQL - [remindee-bot](https://github.com/magnickolas/remindee-bot) | Telegram bot for managing reminders | DB: SQLite - [SophyCore](https://github.com/FarDragi/SophyCore) | Main system that centralizes all rules, to be used by both the discord bot and the future site | DB: Postgres +- [Rustify](https://github.com/vtvz/rustify) | Telegram bot for Spotify with multi-source lyrics, AI analysis, real-time profanity detection and auto-skip | DB: Postgres #### Crypto - [MoonRamp](https://github.com/MoonRamp/MoonRamp) | A free and open source crypto payment gateway | DB: MySQL, Postgres, SQLite -- [Oura Postgres Sink](https://github.com/dcSpark/oura-postgres-sink) | Sync a postgres database with the cardano blockchain using [Oura](https://github.com/txpipe/oura) | DB: Postgres +- [Oura Postgres Sink](https://github.com/dcSpark/oura-postgres-sink) | Sync a postgres database with the cardano blockchain using Oura | DB: Postgres - [RGB Lib](https://github.com/RGB-Tools/rgb-lib) | A library to manage wallets for RGB assets | DB: MySQL, Postgres, SQLite +- [Sensei](https://github.com/L2-Technology/sensei) | A Bitcoin lightning node implementation | DB: MySQL, Postgres, SQLite #### Dev Tools +- [CodeCTRL](https://codectrl.authentura.com) ([repository](https://github.com/Authentura/codectrl)) | A self-hostable code logging platform | DB: SQLite +- [Crosup](https://github.com/tsirysndr/crosup) | Quickly setup your development environment on your Chromebook/ChromeOS or any Linux distribution | DB: SQLite +- [CyberAPI](https://github.com/vicanso/cyberapi) API tool client for developer. | DB: SQLite - [nitro_repo](https://github.com/wyatt-herkamp/nitro_repo) | An OpenSource, lightweight, and fast artifact manager. | DB: MySQL, SQLite +- [Notifico](https://notifico.tech) ([repository](https://github.com/notificohq/notifico)) | An advanced omni-channel notification server. | DB: MySQL, Postgres, SQLite - [Orca](https://github.com/workfoxes/orca) | An No-code Test Automation platform using Actix, SeaORM, React. runs on the desktop and cloud | DB: Postgres #### System - [Email view tracker](https://github.com/friedemannsommer/email-view-tracker) | Simple web UI to create tracking URLs for HTML emails. | DB: MySQL, Postgres - [LLDAP](https://github.com/nitnelave/lldap) ![GitHub stars](https://img.shields.io/github/stars/nitnelave/lldap.svg?style=social) | A light LDAP server for user management | DB: MySQL, Postgres, SQLite - [RSS aggregator](https://github.com/fistons/rss-aggregator)| A small RSS aggregator and API using Actix Web and SeaORM | DB: Postgres +- [ruuth](https://github.com/outurnate/ruuth) ([repository](https://github.com/outurnate/ruuth)) | A simple nginx auth_request backend providing MFA and lockout mechanisms | DB: MySQL, Postgres, SQLite - [snmp-sim-rust](https://github.com/sonalake/snmp-sim-rust) | SNMP Simulator | DB: SQLite - [Wikipedia Speedrun](https://wikipediaspeedrun.com) ([repository](https://github.com/hut8/wikipedia-speedrun)) | Finds shortest paths between Wikipedia articles | DB: SQLite #### Url Shortener - [Dinoly](https://github.com/ippsav/Dinoly) | An url shortener using Axum web framework and SeaORM | DB: Postgres -- [Linx](https://github.com/whizzes/linx) | Headless URL Shortener written in Rust 🦀 -- [SlashURL](https://github.com/henriquekirchheck/slashurl) | A url shortener using Rust designed to be implemented anywere | DB: PostgreSQL +- [SlashURL](https://github.com/henriquekirchheck/slashurl) | A url shortener using Rust designed to be implemented anywhere | DB: PostgreSQL - [url_shortener](https://github.com/michidk/url_shortener) | A simple self-hosted URL shortener written in Rust | DB: MySQL, Postgres, SQLite #### Desktop / CLI Apps - [pansy](https://github.com/niuhuan/pansy) | An illustration app using SeaORM, SQLite, flutter. runs on the desktop and mobile terminals | DB: SQLite +- [Spyglass](https://www.spyglass.fyi/) ([repository](https://github.com/a5huynh/spyglass)) ![GitHub stars](https://img.shields.io/github/stars/a5huynh/spyglass.svg?style=social) | 🔭 A personal search engine that indexes what you want w/ a simple set of rules. | DB: SQLite - [todo-rs](https://github.com/anshulxyz/todo-rs/) | A TUI ToDo-app written in Rust using Cursive library and SeaORM for SQLite | DB: SQLite - [Warpgate](https://github.com/warp-tech/warpgate) ![GitHub stars](https://img.shields.io/github/stars/warp-tech/warpgate.svg?style=social) | Smart SSH bastion that works with any SSH client | DB: SQLite @@ -90,17 +106,18 @@ If you have built an app using SeaORM and want to showcase it, feel free to open ### Frameworks -- [awto](https://github.com/awto-rs/awto) | Awtomate your 🦀 microservices with awto | DB: Postgres +- [actix-admin](https://github.com/mgugger/actix-admin) | An admin panel for Actix Web built with Tera, HTMX and SeaOrm | DB: MySQL, Postgres, SQLite - [poem_admin](https://github.com/lingdu1234/poem_admin) | An admin panel built with poems, SeaORM and Vue 3. | DB: MySQL, Postgres, SQLite -- [Quasar](https://github.com/Technik97/Quasar) | Rust REST API using Actix Web, SeaORM and Postgres with Svelte Typescript Frontend | DB: Postgres +- [Loco.rs](https://github.com/loco-rs/loco) | A full stack Web and API productivity framework similar to Rails, based on SeaORM with db tooling and migrations code generation | DB: Postgres, SQLite - [tardis](https://github.com/ideal-world/tardis) | Elegant, Clean Rust development framework🛸 | DB: MySQL, Postgres, SQLite -- [ZAPP](https://zapp.epics.dev) ([repository](https://github.com/EpicsDAO/zapp)) | ZAPP is a serverless framework made by Rust. Quickly build a scalable GraphQL API web server. +- [spring-rs](https://github.com/spring-rs/spring-rs) | A application framework written in rust inspired by java's spring-boot🍃 | DB: MySQL, Postgres, SQLite +- [qiluo-admin](https://github.com/chelunfu/qiluo_admin) | Axum + SeaORM + JWT + Scheduled + Tasks + SnowId + Redis + Memory + VUE3 | DB: MySQL, Postgres, SQLite ### Scaffolding -- [Axum Book Management](https://github.com/lz1998/axum-book-management) | CRUD system of book-management with ORM and JWT for educational purposes | DB: MySQL - [actix-react-starter-template](https://github.com/aslamplr/actix-react-starter-template) | Actix Web + SeaORM + React + Redux + Redux Saga project starter template | DB: Postgres - [Adta](https://github.com/aaronleopold/adta) | Adta is **A**nother **D**amn **T**odo **A**pp, fun little side project | DB: MySQL, Postgres, SQLite +- [Axum Book Management](https://github.com/lz1998/axum-book-management) | CRUD system of book-management with ORM and JWT for educational purposes | DB: MySQL - [BookStore](https://github.com/AfaanBilal/bookstore) | A bookstore manegement system HTTP REST API using Rocket, SeaORM and JWT. | DB: MySQL - [crud-rs](https://github.com/onichandame/crud-rs) | A framework combining async-graphql and SeaORM - [http-api-rs](https://github.com/daniel-samson/http-api-rs) | Template project for creating REST API's in rust with swagger ui @@ -109,6 +126,7 @@ If you have built an app using SeaORM and want to showcase it, feel free to open - [rust-juniper-playground](https://github.com/Yama-Tomo/rust-juniper-playground) | juniper with SeaORM example | DB: MySQL - [service_auth](https://github.com/shorii/service_auth) | A simple JWT authentication web-application | DB: Postgres - [Super Toolbox](https://github.com/atopx/toolbox) | micro-service best practices: use go-gin and rust-tonic | DB: mysql +- [rsapi](https://github.com/atopx/rsapi) | A lightweight REST API template, Axum + SeaORM + JWT Middleware + Mini DockerFile | DB: Postgres ## Learning Resources diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e290715636..f451091406 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,3 +98,19 @@ Or, you `cd` into `sea-orm-cli` directory and simply execute: ```sh cargo install --force --path . ``` + +Or, you install `sea-orm-cli` from GitHub: + +```sh +cargo install sea-orm-cli --force --git https://github.com/SeaQL/sea-orm --branch +``` + +### TOML formatting + +We use `taplo` to format and lint TOML files across the repository. + +```sh +cargo install --locked taplo-cli +taplo fmt --check +``` + diff --git a/Cargo.toml b/Cargo.toml index f5aaecd688..6d090973e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,35 @@ [workspace] -members = [".", "sea-orm-macros", "sea-orm-codegen"] +members = [".", "sea-orm-macros", "sea-orm-codegen", "sea-orm-arrow"] [package] -name = "sea-orm" -version = "0.12.0" -authors = ["Chris Tsang "] -edition = "2021" -description = "🐚 An async & dynamic ORM for Rust" -license = "MIT OR Apache-2.0" -homepage = "https://www.sea-ql.org/SeaORM" +authors = ["Chris Tsang "] +categories = ["database"] +description = "🐚 An async & dynamic ORM for Rust" documentation = "https://docs.rs/sea-orm" -repository = "https://github.com/SeaQL/sea-orm" -categories = ["database"] -keywords = ["async", "orm", "mysql", "postgres", "sqlite"] -rust-version = "1.65" +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" +keywords = ["async", "orm", "mysql", "postgres", "sqlite"] +license = "MIT OR Apache-2.0" +name = "sea-orm" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.37" [package.metadata.docs.rs] -features = ["default", "sqlx-all", "mock", "runtime-async-std-native-tls", "postgres-array", "sea-orm-internal"] +features = [ + "default", + "sqlx-all", + "mock", + "proxy", + "rbac", + "schema-sync", + "tracing-spans", + "runtime-tokio-native-tls", + "postgres-array", + "postgres-vector", + "with-ipnetwork", + "with-arrow", +] rustdoc-args = ["--cfg", "docsrs"] [lib] @@ -26,41 +39,72 @@ path = "src/lib.rs" [dependencies] async-stream = { version = "0.3", default-features = false } async-trait = { version = "0.1", default-features = false } -chrono = { version = "0.4.20", default-features = false, optional = true } -time = { version = "0.3", default-features = false, optional = true } -futures = { version = "0.3", default-features = false, features = ["std"] } +bigdecimal = { version = "0.4", default-features = false, features = [ + "std", +], optional = true } +chrono = { version = "0.4.30", default-features = false, optional = true } +derive_more = { version = "2", features = ["debug"] } +futures-util = { version = "0.3", default-features = false, features = [ + "std", +] } +inventory = { version = "0.3", optional = true } +ipnetwork = { version = "0.20", default-features = false, optional = true } +itertools = "0.14.0" log = { version = "0.4", default-features = false } -tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } -rust_decimal = { version = "1", default-features = false, optional = true } -bigdecimal = { version = "0.3", default-features = false, optional = true } -sea-orm-macros = { version = "0.12.0", path = "sea-orm-macros", default-features = false, features = ["strum"] } -sea-query = { version = "0.29.0-rc.2", features = ["thread-safe"] } -sea-query-binder = { version = "0.4.0-rc.2", default-features = false, optional = true } -strum = { version = "0.24", default-features = false } +mac_address = { version = "1.1", default-features = false, optional = true } +ouroboros = { version = "0.18", default-features = false } +pgvector = { version = "~0.4", default-features = false, optional = true } +rust_decimal = { version = "1", default-features = false, features = [ + "std", +], optional = true } +sea-orm-arrow = { version = "2.0.0-rc", path = "sea-orm-arrow", default-features = false, optional = true } +sea-orm-macros = { version = "~2.0.0-rc.37", path = "sea-orm-macros", default-features = false, features = [ + "async", + "strum", +] } +sea-query = { version = "=1.0.0-rc.32", default-features = false, features = [ + "thread-safe", + "hashable-value", + "backend-mysql", + "backend-postgres", + "backend-sqlite", + "sea-orm", +] } +sea-query-sqlx = { version = "=0.8.0-rc.14", default-features = false, optional = true } +sea-schema = { version = "0.17.0-rc.15", default-features = false, features = [ + "discovery", + "writer", + "probe", +], optional = true } serde = { version = "1.0", default-features = false } serde_json = { version = "1.0", default-features = false, optional = true } -sqlx = { version = "0.6", default-features = false, optional = true } -uuid = { version = "1", default-features = false, optional = true } -ouroboros = { version = "0.15", default-features = false } +sqlx = { version = "0.8.4", default-features = false, optional = true } +strum = { version = "0.28", default-features = false } +thiserror = { version = "2", default-features = false } +time = { version = "0.3.36", default-features = false, optional = true } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } url = { version = "2.2", default-features = false } -thiserror = { version = "1", default-features = false } +uuid = { version = "1", default-features = false, optional = true } [dev-dependencies] +dotenv = "0.15" +maplit = { version = "1" } +pretty_assertions = { version = "0.7" } +sea-orm = { path = ".", features = [ + "debug-print", + "mock", + "postgres-array", + "tests-cfg", +] } smol = { version = "1.2" } smol-potat = { version = "1.1" } -async-std = { version = "1", features = ["attributes", "tokio1"] } -futures = { version = "0.3" } +time = { version = "0.3.36", features = ["macros"] } tokio = { version = "1.6", features = ["full"] } -actix-rt = { version = "2.2.0" } -maplit = { version = "1" } -rust_decimal_macros = { version = "1" } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -sea-orm = { path = ".", features = ["mock", "debug-print", "tests-cfg", "postgres-array", "sea-orm-internal"] } -pretty_assertions = { version = "0.7" } -time = { version = "0.3", features = ["macros"] } uuid = { version = "1", features = ["v4"] } -once_cell = "1.8" -arraystring = "0.3" [features] debug-print = [] @@ -69,57 +113,129 @@ default = [ "with-json", "with-chrono", "with-rust_decimal", - "with-bigdecimal", "with-uuid", "with-time", + "sqlite-use-returning-for-3_35", ] -macros = ["sea-orm-macros/derive", "sea-query/derive"] +entity-registry = ["inventory", "sea-orm-macros/entity-registry"] +json-array = [ + "postgres-array", +] # this does not actually enable sqlx-postgres, but only a few traits to support array in sea-query +macros = ["sea-orm-macros/derive"] +mariadb-use-returning = [] mock = [] -with-json = ["serde_json", "sea-query/with-json", "chrono?/serde", "time?/serde", "uuid?/serde", "sea-query-binder?/with-json", "sqlx?/json"] -with-chrono = ["chrono", "sea-query/with-chrono", "sea-query-binder?/with-chrono", "sqlx?/chrono"] -with-rust_decimal = ["rust_decimal", "sea-query/with-rust_decimal", "sea-query-binder?/with-rust_decimal", "sqlx?/decimal"] -with-bigdecimal = ["bigdecimal", "sea-query/with-bigdecimal", "sea-query-binder?/with-bigdecimal", "sqlx?/bigdecimal"] -with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-binder?/with-uuid", "sqlx?/uuid"] -with-time = ["time", "sea-query/with-time", "sea-query-binder?/with-time", "sqlx?/time"] -postgres-array = ["sea-query/postgres-array", "sea-query-binder?/postgres-array", "sea-orm-macros/postgres-array"] -sea-orm-internal = [] -sqlx-dep = [] -sqlx-all = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite"] -sqlx-mysql = ["sqlx-dep", "sea-query-binder/sqlx-mysql", "sqlx/mysql"] -sqlx-postgres = ["sqlx-dep", "sea-query-binder/sqlx-postgres", "sqlx/postgres", "postgres-array"] -sqlx-sqlite = ["sqlx-dep", "sea-query-binder/sqlx-sqlite", "sqlx/sqlite"] -runtime-async-std = [] +postgres-array = [ + "sea-query/postgres-array", + "sea-orm-macros/postgres-array", + "sea-query-sqlx?/postgres-array", +] +postgres-use-serial-pk = ["sea-query/option-postgres-use-serial"] +postgres-vector = [ + "pgvector", + "sea-query/postgres-vector", + "sea-query-sqlx?/postgres-vector", +] +proxy = ["serde/derive"] +rbac = ["sea-query/audit", "macros"] +runtime-async-std = ["sqlx?/runtime-async-std"] runtime-async-std-native-tls = [ "sqlx?/runtime-async-std-native-tls", - "sea-query-binder?/runtime-async-std-native-tls", "runtime-async-std", ] runtime-async-std-rustls = [ "sqlx?/runtime-async-std-rustls", - "sea-query-binder?/runtime-async-std-rustls", "runtime-async-std", ] -runtime-actix = [] -runtime-actix-native-tls = [ - "sqlx?/runtime-actix-native-tls", - "sea-query-binder?/runtime-actix-native-tls", - "runtime-actix", -] -runtime-actix-rustls = [ - "sqlx?/runtime-actix-rustls", - "sea-query-binder?/runtime-actix-rustls", - "runtime-actix", +runtime-tokio = ["sqlx?/runtime-tokio"] +runtime-tokio-native-tls = ["sqlx?/runtime-tokio-native-tls", "runtime-tokio"] +runtime-tokio-rustls = ["sqlx?/runtime-tokio-rustls", "runtime-tokio"] +rusqlite = [] +schema-sync = ["sea-schema"] +sea-orm-internal = [] +seaography = ["sea-orm-macros/seaography"] +sqlite-no-row-value-before-3_15 = [] +sqlite-use-returning-for-3_35 = [] +sqlx-all = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite"] +sqlx-dep = ["sqlx"] +sqlx-mysql = [ + "sqlx-dep", + "sea-query-sqlx/sqlx-mysql", + "sea-schema?/sqlx-mysql", ] -runtime-tokio = [] -runtime-tokio-native-tls = [ - "sqlx?/runtime-tokio-native-tls", - "sea-query-binder?/runtime-tokio-native-tls", - "runtime-tokio", +sqlx-postgres = [ + "sqlx-dep", + "sea-query-sqlx/sqlx-postgres", + "postgres-array", + "sea-schema?/sqlx-postgres", ] -runtime-tokio-rustls = [ - "sqlx?/runtime-tokio-rustls", - "sea-query-binder?/runtime-tokio-rustls", - "runtime-tokio", +sqlx-sqlite = [ + "sqlx-dep", + "sea-query-sqlx/sqlx-sqlite", + "sea-schema?/sqlx-sqlite", ] +sync = [] tests-cfg = ["serde/derive"] -seaography = ["sea-orm-macros/seaography"] +tests-features = [ + "default", + "postgres-array", + "rbac", + "schema-sync", + "with-arrow", + "with-bigdecimal", + "with-ipnetwork", +] +tracing-spans = [] +with-arrow = ["sea-orm-arrow", "sea-orm-macros/with-arrow"] +with-bigdecimal = [ + "bigdecimal", + "sea-query/with-bigdecimal", + "sea-query-sqlx?/with-bigdecimal", + "sea-orm-arrow?/with-bigdecimal", +] +with-chrono = [ + "chrono", + "sea-query/with-chrono", + "sea-query-sqlx?/with-chrono", + "sea-orm-arrow?/with-chrono", +] +with-ipnetwork = [ + "ipnetwork", + "sea-query/with-ipnetwork", + "sea-query-sqlx?/with-ipnetwork", +] +with-json = [ + "serde_json", + "sea-query/with-json", + "sea-orm-macros/with-json", + "chrono?/serde", + "rust_decimal?/serde", + "bigdecimal?/serde", + "uuid?/serde", + "time?/serde", + "mac_address?/serde", + "pgvector?/serde", + "sea-query-sqlx?/with-json", +] +with-mac_address = [ + "mac_address", + "sea-query/with-mac_address", + "sea-query-sqlx?/with-mac_address", +] +with-rust_decimal = [ + "rust_decimal", + "sea-query/with-rust_decimal", + "sea-query-sqlx?/with-rust_decimal", + "sea-orm-arrow?/with-rust_decimal", +] +with-time = [ + "time", + "sea-query/with-time", + "sea-query-sqlx?/with-time", + "sea-orm-arrow?/with-time", +] +with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-sqlx?/with-uuid"] + +# This allows us to develop using a local version of sea-query +[patch.crates-io] +# sea-query = { path = "../sea-query" } +# sea-query = { git = "https://github.com/SeaQL/sea-query", branch = "master" } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000000..50e3b1629f --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,63 @@ +## Use a local SeaORM version + +Add this to your `Cargo.toml` for you local project to test out your changes: + +```toml +[patch.crates-io] +sea-orm = { path = "../sea-orm" } +``` + +## Before submitting PR + +### Run `clippy` and `fmt` + +We need nightly to do the formatting, + +```sh +cargo +nightly fmt +``` + +If you don't have nightly then at least run: + +```sh +cargo fmt && git checkout -- ./sea-orm-codegen/src/tests_cfg/ +``` + +We use latest stable clippy: + +```sh +cargo clippy --all +``` + +### Running unit tests + +Just do: + +```sh +cargo test --lib +cargo test --doc +``` + +### Launch some databases + +There is a docker compose under `build-tools`, but usually I just pick the ones I need from `build-tools/docker-crete.sh`: + +```sh +docker run \ + --name "postgres-14" \ + --env POSTGRES_USER="sea" \ + --env POSTGRES_PASSWORD="sea" \ + -d -p 5432:5432 postgres:14 +``` + +### Running integration tests + +You need to supply the right feature flags to run integration tests: + +```sh +DATABASE_URL="sqlite::memory:" cargo test --features sqlx-sqlite,runtime-tokio --test crud_tests +DATABASE_URL="mysql://sea:sea@localhost" cargo test --features sqlx-mysql,runtime-tokio-native-tls --test crud_tests +DATABASE_URL="postgres://sea:sea@localhost" cargo test --features sqlx-postgres,runtime-tokio-native-tls --test crud_tests +``` + +Or use `--tests` to run all tests, which can take a while. diff --git a/LICENSE-MIT b/LICENSE-MIT index 438ca971b1..719424ff8e 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2021 Tsang Hao Fung +Copyright (c) 2023 Seafire Software Limited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 0000000000..5d5266c7fe --- /dev/null +++ b/README-zh.md @@ -0,0 +1,550 @@ +
+ + SeaORM + +

+

一个强大且动态的 Rust ORM

+ + [![crate](https://img.shields.io/crates/v/sea-orm.svg)](https://crates.io/crates/sea-orm) + [![build status](https://github.com/SeaQL/sea-orm/actions/workflows/rust.yml/badge.svg)](https://github.com/SeaQL/sea-orm/actions/workflows/rust.yml) + [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) +
请给我们一个 ⭐ 以支持我们! + +
+ +# 🐚 SeaORM + +SeaORM 是一个关系型 ORM,帮助你在 Rust 中构建 Web 服务,同时提供动态语言的使用体验。 + +### 高级关系 + +以高层次、概念化的方式建模复杂关系:一对一、一对多、多对多,甚至自引用。 + +### 熟悉的概念 + +受 Ruby、Python 和 Node.js 生态系统中流行 ORM 的启发,SeaORM 提供的开发体验让你感觉似曾相识。 + +### 功能丰富 + +SeaORM 是一个功能齐全的 ORM,内置过滤、分页和嵌套查询,加速构建 REST、GraphQL 和 gRPC API。 + +### 生产就绪 + +SeaORM 周下载量超过 25 万次,已被全球的初创企业和大型企业采用,适用于生产环境。 + +## 快速开始 + +[![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) +加入我们的 Discord 服务器,与其他成员交流! + ++ [中文文档](https://www.sea-ql.org/SeaORM/zh-CN/docs/index/) + +集成示例: + ++ [Actix 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) ++ [Axum 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) ++ [GraphQL 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/graphql_example) ++ [jsonrpsee 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/jsonrpsee_example) ++ [Loco 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_example) / [Loco REST 入门](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_starter) ++ [Poem 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) ++ [Rocket 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) / [Rocket OpenAPI 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_okapi_example) ++ [Salvo 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/salvo_example) ++ [Tonic 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/tonic_example) ++ [Seaography 示例 (Bakery)](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) / [Seaography 示例 (Sakila)](https://github.com/SeaQL/seaography/tree/main/examples/sqlite) + +如果你想要一个简洁的单文件示例来展示 SeaORM 的精华,可以试试: ++ [快速入门](https://github.com/SeaQL/sea-orm/blob/master/examples/quickstart/src/main.rs) + +让我们快速了解一下 SeaORM 的独特功能。 + +## 灵活的实体格式 +你不需要手写这些!实体文件可以使用 `sea-orm-cli` 从现有数据库生成, +以下代码通过 `--entity-format dense` 生成 *(2.0 新增)*。 +```rust +mod user { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "user")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub email: String, + #[sea_orm(has_one)] + pub profile: HasOne, + #[sea_orm(has_many)] + pub posts: HasMany, + } +} +mod post { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "post")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub title: String, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub author: HasOne, + #[sea_orm(has_many, via = "post_tag")] // 多对多关系,使用中间表 + pub tags: HasMany, + } +} +``` + +## 智能实体加载器 +实体加载器智能地对一对一关系使用 join,对一对多关系使用 data loader, +即使在执行嵌套查询时也能消除 N+1 问题。 +```rust +// 加载路径: +// user -> profile +// user -> post +// post -> post_tag -> tag +let smart_user = user::Entity::load() + .filter_by_id(42) // 等价于 .filter(user::COLUMN.id.eq(42)) + .with(profile::Entity) // 一对一使用 join + .with((post::Entity, tag::Entity)) // 一对多使用 data loader + .one(db) + .await? + .unwrap(); + +// 底层执行 3 个查询: +// 1. SELECT FROM user JOIN profile WHERE id = $ +// 2. SELECT FROM post WHERE user_id IN (..) +// 3. SELECT FROM tag JOIN post_tag WHERE post_id IN (..) + +smart_user + == user::ModelEx { + id: 42, + name: "Bob".into(), + email: "bob@sea-ql.org".into(), + profile: HasOne::Loaded( + profile::ModelEx { + picture: "image.jpg".into(), + } + .into(), + ), + posts: HasMany::Loaded(vec![post::ModelEx { + title: "Nice weather".into(), + tags: HasMany::Loaded(vec![tag::ModelEx { + tag: "sunny".into(), + }]), + }]), + }; +``` + +## ActiveModel:简化嵌套持久化 +通过流畅的 builder API,在单次操作中持久化整个对象图:用户、个人资料(一对一)、 +帖子(一对多)和标签(多对多)。SeaORM 自动确定依赖关系, +以正确的顺序插入或删除对象。 + +```rust +// 创建上面展示的嵌套对象: +let user = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("image.jpg")) + .add_post( + post::ActiveModel::builder() + .set_title("Nice weather") + .add_tag(tag::ActiveModel::builder().set_tag("sunny")), + ) + .save(db) + .await?; +``` + +## Schema 优先还是实体优先?你的选择 + +SeaORM 提供了强大的迁移系统,让你轻松创建表、修改 Schema 和填充数据。 + +SeaORM 2.0 还提供了一流的[实体优先工作流](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/): +只需定义新实体或向现有实体添加列, +SeaORM 将自动检测变更并创建新的表、列、唯一键和外键。 + +```rust +// SeaORM 解析外键依赖,按拓扑顺序创建表。 +// 需要 `entity-registry` 和 `schema-sync` feature flags。 +db.get_schema_registry("my_crate::entity::*").sync(db).await; +``` + +## 简洁的原生 SQL + +让 SeaORM 处理 95% 的事务查询。 +对于过于复杂而难以表达的剩余情况, +SeaORM 仍然提供便捷的原生 SQL 支持。 +```rust +let user = Item { name: "Bob" }; // 嵌套参数访问 +let ids = [2, 3, 4]; // 通过 `..` 运算符展开 + +let user: Option = user::Entity::find() + .from_raw_sql(raw_sql!( + Sqlite, + r#"SELECT "id", "name" FROM "user" + WHERE "name" LIKE {user.name} + AND "id" in ({..ids}) + "# + )) + .one(db) + .await?; +``` + +## 同步支持 + +[`sea-orm-sync`](https://crates.io/crates/sea-orm-sync) 提供完整的 SeaORM API,无需异步运行时,非常适合使用 SQLite 的轻量级 CLI 程序。 + +参见[快速入门示例](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-sync/examples/quickstart/src/main.rs)了解用法。 + +## 基础操作 + +### 查询 +SeaORM 在实体层面建模一对多和多对多关系, +让你通过中间表在一次调用中遍历多对多链接。 +```rust +// 查找所有模型 +let cakes: Vec = Cake::find().all(db).await?; + +// 查找并过滤 +let chocolate: Vec = Cake::find() + .filter(Cake::COLUMN.name.contains("chocolate")) + .all(db) + .await?; + +// 查找单个模型 +let cheese: Option = Cake::find_by_id(1).one(db).await?; +let cheese: cake::Model = cheese.unwrap(); + +// 查找关联模型(惰性) +let fruit: Option = cheese.find_related(Fruit).one(db).await?; + +// 查找关联模型(急切加载):用于一对一关系 +let cake_with_fruit: Vec<(cake::Model, Option)> = + Cake::find().find_also_related(Fruit).all(db).await?; + +// 查找关联模型(急切加载):同时适用于一对多和多对多关系 +let cake_with_fillings: Vec<(cake::Model, Vec)> = Cake::find() + .find_with_related(Filling) // 多对多关系会执行两次 join + .all(db) // 行会自动按左侧实体合并 + .await?; +``` +### 嵌套查询 + +Partial model 通过只查询所需字段来避免过度获取; +它还使编写深层嵌套的关系查询变得简单。 +```rust +use sea_orm::DerivePartialModel; + +#[derive(DerivePartialModel)] +#[sea_orm(entity = "cake::Entity")] +struct CakeWithFruit { + id: i32, + name: String, + #[sea_orm(nested)] + fruit: Option, // 可以是普通模型或另一个 partial model +} + +let cakes: Vec = Cake::find() + .left_join(fruit::Entity) // 无需指定 join 条件 + .into_partial_model() // 只会查询 partial model 中的列 + .all(db) + .await?; +``` + +### 插入 +SeaORM 的 ActiveModel 让你直接使用 Rust 数据结构, +通过简单的 API 进行持久化。 +批量插入大量不同数据源的行也很方便。 +```rust +let apple = fruit::ActiveModel { + name: Set("Apple".to_owned()), + ..Default::default() // 无需设置主键 +}; + +let pear = fruit::ActiveModel { + name: Set("Pear".to_owned()), + ..Default::default() +}; + +// 插入单个:Active Record 风格 +let apple = apple.insert(db).await?; +apple.id == 1; + +// 插入单个:Repository 风格 +let result = Fruit::insert(apple).exec(db).await?; +result.last_insert_id == 1; + +// 插入多个,返回最后插入的 id +let result = Fruit::insert_many([apple, pear]).exec(db).await?; +result.last_insert_id == Some(2); +``` + +### 高级插入 +你可以利用数据库特有的功能执行 upsert 和幂等插入。 +```rust +// 插入多条并返回(需要数据库支持) +let models: Vec = Fruit::insert_many([apple, pear]) + .exec_with_returning(db) + .await?; +models[0] + == fruit::Model { + id: 1, // 数据库分配的值 + name: "Apple".to_owned(), + cake_id: None, + }; + +// 使用 ON CONFLICT 在主键冲突时忽略,并提供 MySQL 特定的 polyfill +let result = Fruit::insert_many([apple, pear]) + .on_conflict_do_nothing() + .exec(db) + .await?; + +matches!(result, TryInsertResult::Conflicted); +``` + +### 更新 +ActiveModel 通过只更新你修改过的字段来避免竞态条件, +绝不会覆盖未改动的列。 +你还可以使用流畅的查询构建 API 构造复杂的批量更新查询。 +```rust +use sea_orm::sea_query::{Expr, Value}; + +let pear: Option = Fruit::find_by_id(1).one(db).await?; +let mut pear: fruit::ActiveModel = pear.unwrap().into(); + +pear.name = Set("Sweet pear".to_owned()); // 更新单个字段的值 + +// 更新单个:只更新修改过的列 +let pear: fruit::Model = pear.update(db).await?; + +// 更新多个:UPDATE "fruit" SET "cake_id" = "cake_id" + 2 +// WHERE "fruit"."name" LIKE '%Apple%' +Fruit::update_many() + .col_expr(fruit::COLUMN.cake_id, fruit::COLUMN.cake_id.add(2)) + .filter(fruit::COLUMN.name.contains("Apple")) + .exec(db) + .await?; +``` +### 保存 +你可以使用 ActiveModel 执行"插入或更新"操作,轻松组合事务操作。 +```rust +let banana = fruit::ActiveModel { + id: NotSet, + name: Set("Banana".to_owned()), + ..Default::default() +}; + +// 创建,因为主键 `id` 是 `NotSet` +let mut banana = banana.save(db).await?; + +banana.id == Unchanged(2); +banana.name = Set("Banana Mongo".to_owned()); + +// 更新,因为主键 `id` 已存在 +let banana = banana.save(db).await?; +``` +### 删除 +与插入和更新一致的 ActiveModel API。 +```rust +// 删除单个:Active Record 风格 +let orange: Option = Fruit::find_by_id(1).one(db).await?; +let orange: fruit::Model = orange.unwrap(); +orange.delete(db).await?; + +// 删除单个:Repository 风格 +let orange = fruit::ActiveModel { + id: Set(2), + ..Default::default() +}; +fruit::Entity::delete(orange).exec(db).await?; + +// 删除多个:DELETE FROM "fruit" WHERE "fruit"."name" LIKE '%Orange%' +fruit::Entity::delete_many() + .filter(fruit::COLUMN.name.contains("Orange")) + .exec(db) + .await?; + +``` +### 原生 SQL 查询 +`raw_sql!` 宏类似 `format!` 宏,但没有 SQL 注入风险。 +它支持嵌套参数插值、数组和元组展开,甚至重复组, +为构造复杂查询提供了极大的灵活性。 + +```rust +#[derive(FromQueryResult)] +struct CakeWithBakery { + name: String, + #[sea_orm(nested)] + bakery: Option, +} + +#[derive(FromQueryResult)] +struct Bakery { + #[sea_orm(alias = "bakery_name")] + name: String, +} + +let cake_ids = [2, 3, 4]; // 通过 `..` 运算符展开 + +// 可以将原生 SQL 与多种 API 配合使用,包括嵌套查询 +let cake: Option = CakeWithBakery::find_by_statement(raw_sql!( + Sqlite, + r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name" + FROM "cake" + LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id" + WHERE "cake"."id" IN ({..cake_ids})"# +)) +.one(db) +.await?; +``` + +## 🧭 Seaography:即时 GraphQL API + +[Seaography](https://github.com/SeaQL/seaography) 是一个构建在 SeaORM 之上的 GraphQL 框架。 +Seaography 允许你快速构建 GraphQL 解析器。 +只需几个命令,你就可以从 SeaORM 实体启动一个功能完备的 GraphQL 服务器, +包含过滤、分页、关系查询和变更操作! + +查看 [Seaography 示例](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) 了解更多。 + + + +## 🖥️ SeaORM Pro:专业管理面板 + +[SeaORM Pro](https://github.com/SeaQL/sea-orm-pro/) 是一个管理面板解决方案,让你可以快速轻松地为应用程序启动管理面板——不需要前端开发技能,但有当然更好! + +SeaORM Pro 已更新以支持 SeaORM 2.0 的最新功能。 + +特性: + ++ 完整的 CRUD 操作 ++ 基于 React + GraphQL 构建 ++ 内置 GraphQL 解析器 ++ 使用 TOML 配置自定义 UI ++ 基于角色的访问控制 *(2.0 新增)* + +阅读[快速入门](https://www.sea-ql.org/sea-orm-pro/docs/install-and-config/getting-started/)指南了解更多。 + +![](https://raw.githubusercontent.com/SeaQL/sea-orm/refs/heads/master/docs/sea-orm-pro-dark.png#gh-dark-mode-only) +![](https://raw.githubusercontent.com/SeaQL/sea-orm/refs/heads/master/docs/sea-orm-pro-light.png#gh-light-mode-only) + +## SQL Server 支持 + +[SQL Server for SeaORM](https://www.sea-ql.org/SeaORM-X/) 为 MSSQL 提供相同的 SeaORM API。我们移植了所有测试用例和示例,并配有 MSSQL 专属文档。如果你正在构建企业软件,可以[申请商业访问权限](https://forms.office.com/r/1MuRPJmYBR)。目前基于 SeaORM 1.0,但我们会在 SeaORM 2.0 最终发布时为现有用户提供免费升级。 + +## 版本发布 + +SeaORM 2.0 已进入候选发布阶段。我们期待你的试用,并通过[分享反馈](https://github.com/SeaQL/sea-orm/discussions/)来帮助塑造最终版本。 + ++ [变更日志](https://github.com/SeaQL/sea-orm/tree/master/CHANGELOG.md) + +SeaORM 2.0 将是我们迄今最重要的版本——包含一些破坏性变更、大量增强功能,以及对开发者体验的明确聚焦。 + ++ [SeaORM 2.0 先睹为快](https://www.sea-ql.org/blog/2025-09-16-sea-orm-2.0/) ++ [深入了解 SeaORM 2.0](https://www.sea-ql.org/blog/2025-09-24-sea-orm-2.0/) ++ [SeaORM 2.0 中的基于角色的访问控制](https://www.sea-ql.org/blog/2025-09-30-sea-orm-rbac/) ++ [Seaography 2.0:强大且可扩展的 GraphQL 框架](https://www.sea-ql.org/blog/2025-10-08-seaography/) ++ [SeaORM 2.0:新实体格式](https://www.sea-ql.org/blog/2025-10-20-sea-orm-2.0/) ++ [SeaORM 2.0:实体优先工作流](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/) ++ [SeaORM 2.0:强类型列](https://www.sea-ql.org/blog/2025-11-11-sea-orm-2.0/) ++ [SeaORM Pro 2.0 新特性](https://www.sea-ql.org/blog/2025-11-21-whats-new-in-seaormpro-2.0/) ++ [SeaORM 2.0:嵌套 ActiveModel](https://www.sea-ql.org/blog/2025-11-25-sea-orm-2.0/) ++ [SeaORM 2.0 全面介绍](https://www.sea-ql.org/blog/2025-12-05-sea-orm-2.0/) ++ [我们如何让 SeaORM 支持同步](https://www.sea-ql.org/blog/2025-12-12-sea-orm-2.0/) ++ [SeaORM 2.0 迁移指南](https://www.sea-ql.org/blog/2026-01-12-sea-orm-2.0/) ++ [SeaORM 现已支持 Arrow 和 Parquet](https://www.sea-ql.org/blog/2026-02-22-sea-orm-arrow/) ++ [SeaORM 2.0 支持 SQL Server](https://www.sea-ql.org/blog/2026-02-25-sea-orm-x/) + +如果你大量使用 SeaQuery,建议查看我们关于 SeaQuery 1.0 发布的博客文章: + ++ [SeaQuery 1.0 之路](https://www.sea-ql.org/blog/2025-08-30-sea-query-1.0/) + +## 许可证 + +根据以下任一许可证授权: + +- Apache 许可证 2.0 + ([LICENSE-APACHE](LICENSE-APACHE) 或 ) +- MIT 许可证 + ([LICENSE-MIT](LICENSE-MIT) 或 ) + +由你选择。 + +## 贡献 + +除非你明确声明,否则你有意提交的任何贡献,根据 Apache-2.0 许可证的定义,都将按照上述许可证双重授权,且不附带任何附加条款或条件。 + +我们诚挚邀请你参与、贡献,并携手共建 Rust 的未来。 + +向我们的贡献者们致以最真诚的感谢! + +[![Contributors](https://opencollective.com/sea-orm/contributors.svg?width=1000&button=false)](https://github.com/SeaQL/sea-orm/graphs/contributors) + +## 谁在使用 SeaORM? + +以下是一些使用 SeaORM 构建的优秀开源软件的简短列表。欢迎[提交你的项目](https://github.com/SeaQL/sea-orm/blob/master/COMMUNITY.md#built-with-seaorm)! + +| 项目 | GitHub | 标语 | +|---------|--------|---------| +| [Zed](https://github.com/zed-industries/zed) | ![GitHub stars](https://img.shields.io/github/stars/zed-industries/zed.svg?style=social) | 高性能、多人代码编辑器 | +| [OpenObserve](https://github.com/openobserve/openobserve) | ![GitHub stars](https://img.shields.io/github/stars/openobserve/openobserve.svg?style=social) | 开源可观测性平台 | +| [RisingWave](https://github.com/risingwavelabs/risingwave) | ![GitHub stars](https://img.shields.io/github/stars/risingwavelabs/risingwave.svg?style=social) | 流处理和管理平台 | +| [LLDAP](https://github.com/nitnelave/lldap) | ![GitHub stars](https://img.shields.io/github/stars/nitnelave/lldap.svg?style=social) | 轻量级 LDAP 用户管理服务器 | +| [Warpgate](https://github.com/warp-tech/warpgate) | ![GitHub stars](https://img.shields.io/github/stars/warp-tech/warpgate.svg?style=social) | 智能 SSH 堡垒,适用于任何 SSH 客户端 | +| [Svix](https://github.com/svix/svix-webhooks) | ![GitHub stars](https://img.shields.io/github/stars/svix/svix-webhooks.svg?style=social) | 企业级 Webhooks 服务 | +| [Ryot](https://github.com/IgnisDa/ryot) | ![GitHub stars](https://img.shields.io/github/stars/ignisda/ryot.svg?style=social) | 你唯一需要的自托管追踪器 | +| [Lapdev](https://github.com/lapce/lapdev) | ![GitHub stars](https://img.shields.io/github/stars/lapce/lapdev.svg?style=social) | 自托管远程开发环境 | +| [System Initiative](https://github.com/systeminit/si) | ![GitHub stars](https://img.shields.io/github/stars/systeminit/si.svg?style=social) | DevOps 自动化平台 | +| [OctoBase](https://github.com/toeverything/OctoBase) | ![GitHub stars](https://img.shields.io/github/stars/toeverything/OctoBase.svg?style=social) | 轻量级、可扩展、离线协作数据后端 | + +## 赞助 + +[SeaQL.org](https://www.sea-ql.org/) 是一个由热情的开发者运营的独立开源组织。如果你愿意,通过 [GitHub Sponsor](https://github.com/sponsors/SeaQL) 进行小额捐赠将不胜感激,并将大大有助于维持组织的运营。 + +### 金牌赞助商 + + + +
+ +
+ +[QDX](https://qdx.co/) 开创了量子动力学驱动的药物发现,利用人工智能和超级计算加速分子建模。 +我们非常感谢 QDX 赞助 SeaORM 的开发,这是为其数据密集型应用提供支持的 SQL 工具包。 + +### 银牌赞助商 + +我们感谢银牌赞助商:Digital Ocean 赞助我们的服务器,以及 JetBrains 赞助我们的 IDE。 + + + + + +
+ + + +
+ +## 吉祥物 + +Ferris 的朋友,寄居蟹 Terres 是 SeaORM 的官方吉祥物。他的爱好是收集贝壳。 + +Terres + +## 🦀 Rustacean 贴纸包 +Rustacean 贴纸包是表达你对 Rust 热情的完美方式。我们的贴纸采用优质防水乙烯基制成,具有独特的哑光效果。 + +贴纸包内容: + ++ SeaQL 项目的标志:SeaQL、SeaORM、SeaQuery、Seaography ++ 吉祥物:Ferris x 3、寄居蟹 Terres ++ Rustacean 文字标识 + +[支持 SeaQL 并获取贴纸包!](https://www.sea-ql.org/sticker-pack/) 所有收益直接用于 SeaQL 项目的持续开发。 + +
Rustacean Sticker Pack by SeaQL diff --git a/README.md b/README.md index bfc00b0a2c..b87ff33349 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,217 @@
- + SeaORM -

SeaORM

- -

🐚 An async & dynamic ORM for Rust

+

+

SeaORM is a powerful ORM for building web services in Rust

[![crate](https://img.shields.io/crates/v/sea-orm.svg)](https://crates.io/crates/sea-orm) - [![docs](https://docs.rs/sea-orm/badge.svg)](https://docs.rs/sea-orm) [![build status](https://github.com/SeaQL/sea-orm/actions/workflows/rust.yml/badge.svg)](https://github.com/SeaQL/sea-orm/actions/workflows/rust.yml) + [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) +
Support us with a ⭐ !
-# SeaORM +# 🐚 SeaORM -#### SeaORM is a relational ORM to help you build web services in Rust with the familiarity of dynamic languages. +[中文文档](https://github.com/SeaQL/sea-orm/blob/master/README-zh.md) -## Getting Started +### Advanced Relations + +Model complex relationships 1-1, 1-N, M-N, and even self-referential in a high-level, conceptual way. + +### Familiar Concepts + +Inspired by popular ORMs in the Ruby, Python, and Node.js ecosystem, SeaORM offers a developer experience that feels instantly recognizable. + +### Feature Rich + +SeaORM is a batteries-included ORM with filters, pagination, and nested queries to accelerate building REST, GraphQL, and gRPC APIs. -[![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) -If you like what we do, consider starring, commenting, sharing and contributing! +### Production Ready + +With 250k+ weekly downloads, SeaORM is production-ready, trusted by startups and enterprises worldwide. + +## Getting Started [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) -Join our Discord server to chat with others in the SeaQL community! +Join our Discord server to chat with others! -+ [Getting Started](https://www.sea-ql.org/SeaORM/docs/index) -+ [Step-by-step Tutorials](https://www.sea-ql.org/sea-orm-tutorial/) -+ [Cookbook](https://www.sea-ql.org/sea-orm-cookbook/) -+ [Usage Example](https://github.com/SeaQL/sea-orm/tree/master/examples/basic) ++ [Documentation](https://www.sea-ql.org/SeaORM) -Integration examples +Integration examples: -+ [Actix v4 Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) -+ [Actix v3 Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix3_example) ++ [Actix Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) + [Axum Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) + [GraphQL Example](https://github.com/SeaQL/sea-orm/tree/master/examples/graphql_example) + [jsonrpsee Example](https://github.com/SeaQL/sea-orm/tree/master/examples/jsonrpsee_example) ++ [Loco Example](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_example) / [Loco REST Starter](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_starter) + [Poem Example](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) -+ [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) ++ [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) / [Rocket OpenAPI Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_okapi_example) + [Salvo Example](https://github.com/SeaQL/sea-orm/tree/master/examples/salvo_example) + [Tonic Example](https://github.com/SeaQL/sea-orm/tree/master/examples/tonic_example) ++ [Seaography Example (Bakery)](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) / [Seaography Example (Sakila)](https://github.com/SeaQL/seaography/tree/main/examples/sqlite) -## Features +If you want a simple, clean example that fits in a single file that demonstrates the best of SeaORM, you can try: ++ [Quickstart](https://github.com/SeaQL/sea-orm/blob/master/examples/quickstart/src/main.rs) -1. Async +Let's have a quick walk through of the unique features of SeaORM. - Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. - -2. Dynamic +## Expressive Entity format +You don't have to write this by hand! Entity files can be generated from an existing database using `sea-orm-cli`, +following is generated with `--entity-format dense` *(new in 2.0)*. +```rust +mod user { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "user")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub email: String, + #[sea_orm(has_one)] + pub profile: HasOne, + #[sea_orm(has_many)] + pub posts: HasMany, + } +} +mod post { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "post")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub title: String, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub author: HasOne, + #[sea_orm(has_many, via = "post_tag")] // M-N relation with junction + pub tags: HasMany, + } +} +``` - Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. +## Smart Entity Loader +The Entity Loader intelligently uses join for 1-1 and data loader for 1-N relations, +eliminating the N+1 problem even when performing nested queries. +```rust +// join paths: +// user -> profile +// user -> post +// post -> post_tag -> tag +let smart_user = user::Entity::load() + .filter_by_id(42) // shorthand for .filter(user::COLUMN.id.eq(42)) + .with(profile::Entity) // 1-1 uses join + .with((post::Entity, tag::Entity)) // 1-N uses data loader + .one(db) + .await? + .unwrap(); + +// 3 queries are executed under the hood: +// 1. SELECT FROM user JOIN profile WHERE id = $ +// 2. SELECT FROM post WHERE user_id IN (..) +// 3. SELECT FROM tag JOIN post_tag WHERE post_id IN (..) + +smart_user + == user::ModelEx { + id: 42, + name: "Bob".into(), + email: "bob@sea-ql.org".into(), + profile: HasOne::Loaded( + profile::ModelEx { + picture: "image.jpg".into(), + } + .into(), + ), + posts: HasMany::Loaded(vec![post::ModelEx { + title: "Nice weather".into(), + tags: HasMany::Loaded(vec![tag::ModelEx { + tag: "sunny".into(), + }]), + }]), + }; +``` -3. Testable +## ActiveModel: nested persistence made simple +Persist an entire object graph: user, profile (1-1), posts (1-N), and tags (M-N) +in a single operation using a fluent builder API. SeaORM automatically determines +the dependencies and inserts or deletes objects in the correct order. - Use mock connections to write unit tests for your logic. +```rust +// this creates the nested object as shown above: +let user = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("image.jpg")) + .add_post( + post::ActiveModel::builder() + .set_title("Nice weather") + .add_tag(tag::ActiveModel::builder().set_tag("sunny")), + ) + .save(db) + .await?; +``` -4. Service Oriented +## Schema first or Entity first? Your choice - Quickly build services that join, filter, sort and paginate data in APIs. +SeaORM provides a powerful migration system that lets you create tables, modify schemas, and seed data with ease. -## A quick taste of SeaORM +With SeaORM 2.0, you also get a first-class [Entity First Workflow](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/): +simply define new entities or add columns to existing ones, +and SeaORM will automatically detect the changes and create the new tables, columns, unique keys, and foreign keys. -### Entity ```rust -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "cake")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - pub name: String, -} +// SeaORM resolves foreign key dependencies and creates the tables in topological order. +// Requires the `entity-registry` and `schema-sync` feature flags. +db.get_schema_registry("my_crate::entity::*").sync(db).await; +``` -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::fruit::Entity")] - Fruit, -} +## Ergonomic Raw SQL -impl Related for Entity { - fn to() -> RelationDef { - Relation::Fruit.def() - } -} +Let SeaORM handle 95% of your transactional queries. +For the remaining cases that are too complex to express, +SeaORM still offers convenient support for writing raw SQL. +```rust +let user = Item { name: "Bob" }; // nested parameter access +let ids = [2, 3, 4]; // expanded by the `..` operator + +let user: Option = user::Entity::find() + .from_raw_sql(raw_sql!( + Sqlite, + r#"SELECT "id", "name" FROM "user" + WHERE "name" LIKE {user.name} + AND "id" in ({..ids}) + "# + )) + .one(db) + .await?; ``` +## Synchronous Support + +[`sea-orm-sync`](https://crates.io/crates/sea-orm-sync) provides the full SeaORM API without requiring an async runtime, making it ideal for lightweight CLI programs with SQLite. + +See the [quickstart example](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-sync/examples/quickstart/src/main.rs) for usage. + +## Basics + ### Select +SeaORM models 1-N and M-N relationships at the Entity level, +letting you traverse many-to-many links through a junction table in a single call. ```rust // find all models let cakes: Vec = Cake::find().all(db).await?; // find and filter let chocolate: Vec = Cake::find() - .filter(cake::Column::Name.contains("chocolate")) + .filter(Cake::COLUMN.name.contains("chocolate")) .all(db) .await?; @@ -102,14 +220,45 @@ let cheese: Option = Cake::find_by_id(1).one(db).await?; let cheese: cake::Model = cheese.unwrap(); // find related models (lazy) -let fruits: Vec = cheese.find_related(Fruit).all(db).await?; +let fruit: Option = cheese.find_related(Fruit).one(db).await?; -// find related models (eager) -let cake_with_fruits: Vec<(cake::Model, Vec)> = - Cake::find().find_with_related(Fruit).all(db).await?; +// find related models (eager): for 1-1 relations +let cake_with_fruit: Vec<(cake::Model, Option)> = + Cake::find().find_also_related(Fruit).all(db).await?; +// find related models (eager): works for both 1-N and M-N relations +let cake_with_fillings: Vec<(cake::Model, Vec)> = Cake::find() + .find_with_related(Filling) // for M-N relations, two joins are performed + .all(db) // rows are automatically consolidated by left entity + .await?; ``` +### Nested Select + +Partial models prevent overfetching by letting you querying only the fields +you need; it also makes writing deeply nested relational queries simple. +```rust +use sea_orm::DerivePartialModel; + +#[derive(DerivePartialModel)] +#[sea_orm(entity = "cake::Entity")] +struct CakeWithFruit { + id: i32, + name: String, + #[sea_orm(nested)] + fruit: Option, // this can be a regular or another partial model +} + +let cakes: Vec = Cake::find() + .left_join(fruit::Entity) // no need to specify join condition + .into_partial_model() // only the columns in the partial model will be selected + .all(db) + .await?; +``` + ### Insert +SeaORM's ActiveModel lets you work directly with Rust data structures and +persist them through a simple API. +It's easy to insert large batches of rows from different data sources. ```rust let apple = fruit::ActiveModel { name: Set("Apple".to_owned()), @@ -121,33 +270,67 @@ let pear = fruit::ActiveModel { ..Default::default() }; -// insert one -let pear = pear.insert(db).await?; +// insert one: Active Record style +let apple = apple.insert(db).await?; +apple.id == 1; + +// insert one: repository style +let result = Fruit::insert(apple).exec(db).await?; +result.last_insert_id == 1; + +// insert many returning last insert id +let result = Fruit::insert_many([apple, pear]).exec(db).await?; +result.last_insert_id == Some(2); +``` + +### Insert (advanced) +You can take advantage of database specific features to perform upsert and idempotent insert. +```rust +// insert many with returning (if supported by database) +let models: Vec = Fruit::insert_many([apple, pear]) + .exec_with_returning(db) + .await?; +models[0] + == fruit::Model { + id: 1, // database assigned value + name: "Apple".to_owned(), + cake_id: None, + }; + +// insert with ON CONFLICT on primary key do nothing, with MySQL specific polyfill +let result = Fruit::insert_many([apple, pear]) + .on_conflict_do_nothing() + .exec(db) + .await?; -// insert many -Fruit::insert_many([apple, pear]).exec(db).await?; +matches!(result, TryInsertResult::Conflicted); ``` + ### Update +ActiveModel avoids race conditions by updating only the fields you've changed, +never overwriting untouched columns. +You can also craft complex bulk update queries with a fluent query building API. ```rust use sea_orm::sea_query::{Expr, Value}; let pear: Option = Fruit::find_by_id(1).one(db).await?; let mut pear: fruit::ActiveModel = pear.unwrap().into(); -pear.name = Set("Sweet pear".to_owned()); +pear.name = Set("Sweet pear".to_owned()); // update value of a single field -// update one +// update one: only changed columns will be updated let pear: fruit::Model = pear.update(db).await?; -// update many: UPDATE "fruit" SET "cake_id" = NULL WHERE "fruit"."name" LIKE '%Apple%' +// update many: UPDATE "fruit" SET "cake_id" = "cake_id" + 2 +// WHERE "fruit"."name" LIKE '%Apple%' Fruit::update_many() - .col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None))) - .filter(fruit::Column::Name.contains("Apple")) + .col_expr(fruit::COLUMN.cake_id, fruit::COLUMN.cake_id.add(2)) + .filter(fruit::COLUMN.name.contains("Apple")) .exec(db) .await?; - ``` ### Save +You can perform "insert or update" operation with ActiveModel, making it easy to compose transactional operations. ```rust let banana = fruit::ActiveModel { id: NotSet, @@ -158,60 +341,127 @@ let banana = fruit::ActiveModel { // create, because primary key `id` is `NotSet` let mut banana = banana.save(db).await?; +banana.id == Unchanged(2); banana.name = Set("Banana Mongo".to_owned()); -// update, because primary key `id` is `Set` +// update, because primary key `id` is present let banana = banana.save(db).await?; - ``` ### Delete +The same ActiveModel API consistent with insert and update. ```rust -// delete one -let orange: Option = Fruit::find_by_id(1).one(db).await?; -let orange: fruit::Model = orange.unwrap(); -fruit::Entity::delete(orange.into_active_model()) - .exec(db) - .await?; - -// or simply +// delete one: Active Record style let orange: Option = Fruit::find_by_id(1).one(db).await?; let orange: fruit::Model = orange.unwrap(); orange.delete(db).await?; -// delete many: DELETE FROM "fruit" WHERE "fruit"."name" LIKE 'Orange' +// delete one: repository style +let orange = fruit::ActiveModel { + id: Set(2), + ..Default::default() +}; +fruit::Entity::delete(orange).exec(db).await?; + +// delete many: DELETE FROM "fruit" WHERE "fruit"."name" LIKE '%Orange%' fruit::Entity::delete_many() - .filter(fruit::Column::Name.contains("Orange")) + .filter(fruit::COLUMN.name.contains("Orange")) .exec(db) .await?; ``` +### Raw SQL Query +The `raw_sql!` macro is like the `format!` macro but without the risk of SQL injection. +It supports nested parameter interpolation, array and tuple expansion, and even repeating group, +offering great flexibility in crafting complex queries. -## Learn More +```rust +#[derive(FromQueryResult)] +struct CakeWithBakery { + name: String, + #[sea_orm(nested)] + bakery: Option, +} -1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md) -1. [Architecture](https://www.sea-ql.org/SeaORM/docs/internal-design/architecture/) -1. [Release Model](https://www.sea-ql.org/SeaORM/blog/2021-08-30-release-model) -1. [Change Log](https://github.com/SeaQL/sea-orm/tree/master/CHANGELOG.md) +#[derive(FromQueryResult)] +struct Bakery { + #[sea_orm(alias = "bakery_name")] + name: String, +} -## Who's using SeaORM? +let cake_ids = [2, 3, 4]; // expanded by the `..` operator + +// can use many APIs with raw SQL, including nested select +let cake: Option = CakeWithBakery::find_by_statement(raw_sql!( + Sqlite, + r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name" + FROM "cake" + LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id" + WHERE "cake"."id" IN ({..cake_ids})"# +)) +.one(db) +.await?; +``` + +## 🧭 Seaography: instant GraphQL API + +[Seaography](https://github.com/SeaQL/seaography) is a GraphQL framework built for SeaORM. +Seaography allows you to build GraphQL resolvers quickly. +With just a few commands, you can launch a fullly-featured GraphQL server from SeaORM entities, +complete with filter, pagination, relational queries and mutations! + +Look at the [Seaography Example](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) to learn more. + + + +## 🖥️ SeaORM Pro: Professional Admin Panel + +[SeaORM Pro](https://github.com/SeaQL/sea-orm-pro/) is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have! + +SeaORM Pro has been updated to support the latest features in SeaORM 2.0. + +Features: + ++ Full CRUD ++ Built on React + GraphQL ++ Built-in GraphQL resolver ++ Customize the UI with TOML config ++ Role Based Access Control *(new in 2.0)* -The following products are powered by SeaORM: +Read the [Getting Started](https://www.sea-ql.org/sea-orm-pro/docs/install-and-config/getting-started/) guide to learn more. - - - - - - - - -


A lightweight web security auditing toolkit

The enterprise ready webhooks service

A personal search engine
+![](https://raw.githubusercontent.com/SeaQL/sea-orm/refs/heads/master/docs/sea-orm-pro-dark.png#gh-dark-mode-only) +![](https://raw.githubusercontent.com/SeaQL/sea-orm/refs/heads/master/docs/sea-orm-pro-light.png#gh-light-mode-only) -SeaORM is the foundation of: -+ [StarfishQL](https://github.com/SeaQL/starfish-ql): an experimental graph database -+ [Seaography](https://github.com/SeaQL/seaography): GraphQL framework for SeaORM +## SQL Server Support -For more projects, see [Built with SeaORM](https://github.com/SeaQL/sea-orm/blob/master/COMMUNITY.md#built-with-seaorm). +[SQL Server for SeaORM](https://www.sea-ql.org/SeaORM-X/) offers the same SeaORM API for MSSQL. We ported all test cases and examples, complemented by MSSQL specific documentation. If you are building enterprise software, you can [request commercial access](https://forms.office.com/r/1MuRPJmYBR). It is currently based on SeaORM 1.0, but we will offer free upgrade to existing users when SeaORM 2.0 is finalized. + +## Releases + +SeaORM 2.0 has reached its release candidate phase. We'd love for you to try it out and help shape the final release by [sharing your feedback](https://github.com/SeaQL/sea-orm/discussions/). + ++ [Change Log](https://github.com/SeaQL/sea-orm/tree/master/CHANGELOG.md) + +SeaORM 2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. + ++ [A Sneak Peek at SeaORM 2.0](https://www.sea-ql.org/blog/2025-09-16-sea-orm-2.0/) ++ [SeaORM 2.0: A closer look](https://www.sea-ql.org/blog/2025-09-24-sea-orm-2.0/) ++ [Role Based Access Control in SeaORM 2.0](https://www.sea-ql.org/blog/2025-09-30-sea-orm-rbac/) ++ [Seaography 2.0: A Powerful and Extensible GraphQL Framework](https://www.sea-ql.org/blog/2025-10-08-seaography/) ++ [SeaORM 2.0: New Entity Format](https://www.sea-ql.org/blog/2025-10-20-sea-orm-2.0/) ++ [SeaORM 2.0: Entity First Workflow](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/) ++ [SeaORM 2.0: Strongly-Typed Column](https://www.sea-ql.org/blog/2025-11-11-sea-orm-2.0/) ++ [What's new in SeaORM Pro 2.0](https://www.sea-ql.org/blog/2025-11-21-whats-new-in-seaormpro-2.0/) ++ [SeaORM 2.0: Nested ActiveModel](https://www.sea-ql.org/blog/2025-11-25-sea-orm-2.0/) ++ [A walk-through of SeaORM 2.0](https://www.sea-ql.org/blog/2025-12-05-sea-orm-2.0/) ++ [How we made SeaORM synchronous](https://www.sea-ql.org/blog/2025-12-12-sea-orm-2.0/) ++ [SeaORM 2.0 Migration Guide](https://www.sea-ql.org/blog/2026-01-12-sea-orm-2.0/) ++ [SeaORM now supports Arrow & Parquet](https://www.sea-ql.org/blog/2026-02-22-sea-orm-arrow/) ++ [SeaORM 2.0 with SQL Server Support](https://www.sea-ql.org/blog/2026-02-25-sea-orm-x/) + +If you make extensive use of SeaQuery, we recommend checking out our blog post on SeaQuery 1.0 release: + ++ [The road to SeaQuery 1.0](https://www.sea-ql.org/blog/2025-08-30-sea-query-1.0/) ## License @@ -230,14 +480,74 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -SeaORM is a community driven project. We welcome you to participate, contribute and together help build Rust's future. +We invite you to participate, contribute and together help build Rust's future. -A big shout out to our contributors: +A big shout out to our contributors! [![Contributors](https://opencollective.com/sea-orm/contributors.svg?width=1000&button=false)](https://github.com/SeaQL/sea-orm/graphs/contributors) +## Who's using SeaORM? + +Here is a short list of awesome open source software built with SeaORM. Feel free to [submit yours](https://github.com/SeaQL/sea-orm/blob/master/COMMUNITY.md#built-with-seaorm)! + +| Project | GitHub | Tagline | +|---------|--------|---------| +| [Zed](https://github.com/zed-industries/zed) | ![GitHub stars](https://img.shields.io/github/stars/zed-industries/zed.svg?style=social) | A high-performance, multiplayer code editor | +| [OpenObserve](https://github.com/openobserve/openobserve) | ![GitHub stars](https://img.shields.io/github/stars/openobserve/openobserve.svg?style=social) | Open-source observability platform | +| [RisingWave](https://github.com/risingwavelabs/risingwave) | ![GitHub stars](https://img.shields.io/github/stars/risingwavelabs/risingwave.svg?style=social) | Stream processing and management platform | +| [LLDAP](https://github.com/nitnelave/lldap) | ![GitHub stars](https://img.shields.io/github/stars/nitnelave/lldap.svg?style=social) | A light LDAP server for user management | +| [Warpgate](https://github.com/warp-tech/warpgate) | ![GitHub stars](https://img.shields.io/github/stars/warp-tech/warpgate.svg?style=social) | Smart SSH bastion that works with any SSH client | +| [Svix](https://github.com/svix/svix-webhooks) | ![GitHub stars](https://img.shields.io/github/stars/svix/svix-webhooks.svg?style=social) | The enterprise ready webhooks service | +| [Ryot](https://github.com/IgnisDa/ryot) | ![GitHub stars](https://img.shields.io/github/stars/ignisda/ryot.svg?style=social) | The only self hosted tracker you will ever need | +| [Lapdev](https://github.com/lapce/lapdev) | ![GitHub stars](https://img.shields.io/github/stars/lapce/lapdev.svg?style=social) | Self-hosted remote development enviroment | +| [System Initiative](https://github.com/systeminit/si) | ![GitHub stars](https://img.shields.io/github/stars/systeminit/si.svg?style=social) | DevOps Automation Platform | +| [OctoBase](https://github.com/toeverything/OctoBase) | ![GitHub stars](https://img.shields.io/github/stars/toeverything/OctoBase.svg?style=social) | A light-weight, scalable, offline collaborative data backend | + +## Sponsorship + +[SeaQL.org](https://www.sea-ql.org/) is an independent open-source organization run by passionate developers. +If you feel generous, a small donation via [GitHub Sponsor](https://github.com/sponsors/SeaQL) will be greatly appreciated, and goes a long way towards sustaining the organization. + +### Gold Sponsors + + + +
+ +
+ +[QDX](https://qdx.co/) pioneers quantum dynamics-powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. +We're immensely grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications. + +### Silver Sponsors + +We're grateful to our silver sponsors: Digital Ocean, for sponsoring our servers. And JetBrains, for sponsoring our IDE. + + + + + +
+ + + +
+ ## Mascot A friend of Ferris, Terres the hermit crab is the official mascot of SeaORM. His hobby is collecting shells. Terres + +## 🦀 Rustacean Sticker Pack +The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish. + +Sticker Pack Contents: + ++ Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography ++ Mascots: Ferris the Crab x 3, Terres the Hermit Crab ++ The Rustacean wordmark + +[Support SeaQL and get a Sticker Pack!](https://www.sea-ql.org/sticker-pack/) All proceeds contributes directly to the ongoing development of SeaQL projects. + +Rustacean Sticker Pack by SeaQL diff --git a/VERSIONS.md b/VERSIONS.md new file mode 100644 index 0000000000..c826d67d7c --- /dev/null +++ b/VERSIONS.md @@ -0,0 +1,8 @@ +# Known good version combo + +sea-orm sea-query sea-query-derive sea-query-sqlx sea-schema +2.0.0-rc.1 1.0.0-rc.6 1.0.0-rc.5 sea-query-binder 0.17.0-rc.3 +2.0.0-rc.4 1.0.0-rc.12 1.0.0-rc.9 0.8.0-rc.8 0.17.0-rc.6 +2.0.0-rc.9 1.0.0-rc.14 1.0.0-rc.9 0.8.0-rc.9 0.17.0-rc.8 +2.0.0-rc.24 1.0.0-rc.27 1.0.0-rc.11 0.8.0-rc.10 0.17.0-rc.17 +2.0.0-rc.27 1.0.0-rc.29 1.0.0-rc.11 0.8.0-rc.11 0.17.0-rc.17 \ No newline at end of file diff --git a/build-tools/back-async.rs b/build-tools/back-async.rs new file mode 100644 index 0000000000..c968706d56 --- /dev/null +++ b/build-tools/back-async.rs @@ -0,0 +1 @@ +cp sea-orm-sync/src/driver/rusqlite.rs ./src/driver/ \ No newline at end of file diff --git a/build-tools/bump-version.sh b/build-tools/bump-version.sh deleted file mode 100644 index ba9657c7a1..0000000000 --- a/build-tools/bump-version.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -e - -# Bump `sea-orm-codegen` version -cd sea-orm-codegen -sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml -cd .. - -# Bump `sea-orm-cli` version -cd sea-orm-cli -sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml -sed -i 's/^sea-orm-codegen [^,]*,/sea-orm-codegen = { version = "\='$1'",/' Cargo.toml -cd .. - -# Bump `sea-orm-macros` version -cd sea-orm-macros -sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml -cd .. - -# Bump `sea-orm` version -sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml -sed -i 's/^sea-orm-macros [^,]*,/sea-orm-macros = { version = "'$1'",/' Cargo.toml - -# Bump `sea-orm-migration` version -cd sea-orm-migration -sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml -sed -i 's/^sea-orm-cli [^,]*,/sea-orm-cli = { version = "'$1'",/' Cargo.toml -sed -i 's/^sea-orm [^,]*,/sea-orm = { version = "'$1'",/' Cargo.toml -cd .. - -git commit -am "$1" - -# Bump examples' dependency version -cd examples -find . -depth -type f -name '*.toml' -exec sed -i 's/^version = ".*" # sea-orm version$/version = "'$1'" # sea-orm version/' {} \; -find . -depth -type f -name '*.toml' -exec sed -i 's/^version = ".*" # sea-orm-migration version$/version = "'$1'" # sea-orm-migration version/' {} \; -git commit -am "update examples" \ No newline at end of file diff --git a/build-tools/bump.sh b/build-tools/bump.sh new file mode 100644 index 0000000000..1478543d8f --- /dev/null +++ b/build-tools/bump.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +# Bump `sea-orm-codegen` version +cd sea-orm-codegen +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +cd .. + +# Bump `sea-orm-cli` version +cd sea-orm-cli +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +sed -i 's/^sea-orm-codegen [^,]*,/sea-orm-codegen = { version = "\='$1'",/' Cargo.toml +cd .. + +# Bump `sea-orm-macros` version +cd sea-orm-macros +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +cd .. + +# Bump `sea-orm` version +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +sed -i 's/^sea-orm-macros [^,]*,/sea-orm-macros = { version = "'~$1'",/' Cargo.toml + +# Bump `sea-orm-migration` version +cd sea-orm-migration +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +sed -i 's/^sea-orm-cli [^,]*,/sea-orm-cli = { version = "'~$1'",/' Cargo.toml +sed -i 's/^sea-orm [^,]*,/sea-orm = { version = "'~$1'",/' Cargo.toml +cd .. + +# Bump `sea-orm-sync` version +cd sea-orm-sync +sed -i 's/^version.*$/version = "'$1'"/' Cargo.toml +cd .. + +git commit -am "$1" + +# Bump examples' dependency version +cd examples +find . -depth -type f -name '*.toml' -exec sed -i 's/^version = ".*" # sea-orm version$/version = "'~$1'" # sea-orm version/' {} \; +find . -depth -type f -name '*.toml' -exec sed -i 's/^version = ".*" # sea-orm-migration version$/version = "'~$1'" # sea-orm-migration version/' {} \; +git add . +git commit -m "update examples" \ No newline at end of file diff --git a/build-tools/cargo-clean.sh b/build-tools/clean.sh similarity index 100% rename from build-tools/cargo-clean.sh rename to build-tools/clean.sh diff --git a/build-tools/del-rel-dep.sh b/build-tools/del-rel-dep.sh new file mode 100644 index 0000000000..d507fc2760 --- /dev/null +++ b/build-tools/del-rel-dep.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +find examples/ -depth -type f -name '*.toml' -exec sed -i '/^path = "..\/..\/..\/sea-orm-migration"/d' {} \; +find examples/ -depth -type f -name '*.toml' -exec sed -i '/^path = "..\/..\/..\/"/d' {} \; \ No newline at end of file diff --git a/build-tools/docker-create.sh b/build-tools/docker-create.sh index cf5aac4ea0..7175daf6e0 100644 --- a/build-tools/docker-create.sh +++ b/build-tools/docker-create.sh @@ -3,7 +3,7 @@ # Delete all containers # $ docker rm -f $(docker ps -a -q) # -# Delete all volumns +# Delete all volumes # $ docker volume rm $(docker volume ls -q) # # Delete all images @@ -65,23 +65,37 @@ docker stop "mysql-5.7" # Setup PostgreSQL +docker run \ + --name "postgres-vector-14" \ + --env POSTGRES_USER="sea" \ + --env POSTGRES_PASSWORD="sea" \ + -d -p 5432:5432 pgvector/pgvector:pg14 +docker stop "postgres-vector-14" + +docker run \ + --name "postgres-14" \ + --env POSTGRES_USER="sea" \ + --env POSTGRES_PASSWORD="sea" \ + -d -p 5432:5432 postgres:14 +docker stop "postgres-14" + docker run \ --name "postgres-13" \ - --env POSTGRES_USER="root" \ - --env POSTGRES_PASSWORD="root" \ + --env POSTGRES_USER="sea" \ + --env POSTGRES_PASSWORD="sea" \ -d -p 5432:5432 postgres:13 docker stop "postgres-13" docker run \ --name "postgres-12" \ - --env POSTGRES_USER="root" \ - --env POSTGRES_PASSWORD="root" \ + --env POSTGRES_USER="sea" \ + --env POSTGRES_PASSWORD="sea" \ -d -p 5432:5432 postgres:12 docker stop "postgres-12" docker run \ --name "postgres-11" \ - --env POSTGRES_USER="root" \ - --env POSTGRES_PASSWORD="root" \ + --env POSTGRES_USER="sea" \ + --env POSTGRES_PASSWORD="sea" \ -d -p 5432:5432 postgres:11 docker stop "postgres-11" diff --git a/build-tools/make-sync.sh b/build-tools/make-sync.sh new file mode 100644 index 0000000000..30257f5887 --- /dev/null +++ b/build-tools/make-sync.sh @@ -0,0 +1,64 @@ +rm -rf sea-orm-sync/src +rm -rf sea-orm-sync/tests +cp -r src sea-orm-sync +cp -r tests sea-orm-sync +cp -r examples/quickstart/src/main.rs sea-orm-sync/examples/quickstart/src/main.rs +rm -rf sea-orm-sync/src/bin +cd sea-orm-sync +find src -type f -name '*.rs' -exec sed -i '' "s/Pin, DbErr>> + 'a + Send>>/Result, DbErr>/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/Pin> + Send + 'b>>/Result/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/Pin> + Send + 'c>>/Result/" {} + +find src -type f -name '*.rs' -exec sed -i '' 's/Box::pin(async move {/({/' {} + +find tests -type f -name '*.rs' -exec sed -i '' 's/Box::pin(async move {/({/' {} + +find src -type f -name '*.rs' -exec sed -i '' 's/async //' {} + +find tests -type f -name '*.rs' -exec sed -i '' 's/async //' {} + +find examples -type f -name '*.rs' -exec sed -i '' 's/async //' {} + +find src -type f -name '*.rs' -exec sed -i '' 's/\.await//' {} + +find tests -type f -name '*.rs' -exec sed -i '' 's/\.await//' {} + +find examples -type f -name '*.rs' -exec sed -i '' 's/\.await//' {} + +find src -type f -name '*.rs' -exec sed -i '' '/#\[async_trait::async_trait\]/d' {} + +find tests -type f -name '*.rs' -exec sed -i '' '/#\[async_trait::async_trait\]/d' {} + +find src -type f -name '*.rs' -exec sed -i '' 's/#\[smol_potat::test\]/#\[test\]/' {} + +find src -type f -name '*.rs' -exec sed -i '' '/#\[smol_potat::main\]/d' {} + +find examples -type f -name '*.rs' -exec sed -i '' '/#\[tokio::main\]/d' {} + +find src -type f -name '*.rs' -exec sed -i '' 's/#\[tokio::test\]/#\[test\]/' {} + +find tests -type f -name '*.rs' -exec sed -i '' 's/#\[cfg(feature = "sqlx-sqlite")\]/#\[cfg(feature = "rusqlite")\]/' {} + +find src -type f -name '*.rs' -exec sed -i '' "/[a-zA-Z]+: Send + Sync,/d" {} + +find src -type f -name '*.rs' -exec sed -i '' "/[a-zA-Z]+: Send,/d" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/: Send + Sync {/ {/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/type Stream<'a>: Stream/type Stream<'a>: Iterator/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/: Send {/ {/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/>: Send$/>/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/: Sync {/ {/" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/Send + Sync + //" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/ + Sync//" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/ + Send//" {} + +find src -type f -name '*.rs' -exec sed -i '' "s/Send + //" {} + +find src -type f -name '*.rs' -exec sed -i '' 's/Arc/Arc/' {} + +find src -type f -name '*.rs' -exec sed -i '' '/T: Send,/d' {} + +find src -type f -name '*.rs' -exec sed -i '' '/R::Model: Send,/d' {} + +find src -type f -name '*.rs' -exec sed -i '' '/S::Item: Send,/d' {} + +find src -type f -name '*.rs' -exec sed -i '' "s/Box::pin/Box::new/" {} + +find src -type f -name '*.rs' -exec sed -i '' 's/impl Stream README.md \ No newline at end of file +cargo readme --no-badges --no-indent-headings --no-license --no-template --no-title > README.md +cd sea-orm-sync && cargo readme --no-badges --no-indent-headings --no-license --no-template --no-title > README.md \ No newline at end of file diff --git a/build-tools/rustfmt.sh b/build-tools/rustfmt.sh index 712135369b..06fc76dcf3 100644 --- a/build-tools/rustfmt.sh +++ b/build-tools/rustfmt.sh @@ -1,5 +1,6 @@ #!/bin/bash set -e +taplo fmt if [ -d ./build-tools ]; then targets=( "Cargo.toml" diff --git a/build-tools/update-strum-macros.sh b/build-tools/update-strum-macros.sh new file mode 100644 index 0000000000..b5fa10c800 --- /dev/null +++ b/build-tools/update-strum-macros.sh @@ -0,0 +1,8 @@ +rm -rf sea-orm-macros/src/strum/helpers +rm -rf sea-orm-macros/src/strum/enum_iter.rs + +cp -r ../strum/strum_macros/src/helpers sea-orm-macros/src/strum/helpers +cp -r ../strum/strum_macros/src/macros/enum_iter.rs sea-orm-macros/src/strum/enum_iter.rs + +sed -i 's/crate::helpers::{*/super::helpers::{/' sea-orm-macros/src/strum/enum_iter.rs +sed -i 's/parse_quote!(::strum)*/parse_quote!(sea_orm::strum)/' sea-orm-macros/src/strum/helpers/type_props.rs \ No newline at end of file diff --git a/changelog/2.0.0-rc.20.md b/changelog/2.0.0-rc.20.md new file mode 100644 index 0000000000..e3cd2201da --- /dev/null +++ b/changelog/2.0.0-rc.20.md @@ -0,0 +1,49 @@ +## Release Notes: SeaORM 2.0.0-rc.20 + +*(since 2.0.0-rc.19)* + +### New Features + +**Allow more stringy newtypes in `DeriveValueType`** (#2831) + +- Produce syntax-aware attribute errors in `DeriveValueType` macro +- Allow `value_type` attr for named field structs in `DeriveValueType` +- Assume no derive attributes means `DeriveValueTypeStruct` + +**Many-to-many self-referencing relation** (#2823) + +- Add primary_key check to `DeriveEntityModel` +- Support `RelatedSelfVia` and `self_ref` in dense entity format +- Support `self_ref` on compact_model and in reverse +- Loader support for self via in reverse + +**ActiveModelEx: support `self_ref`** + +ActiveModelEx now supports self-referencing relations. + +**Add nullable column types** + +Support for nullable column types added. + +### Bug Fixes + +**Fix missing casts when returning one** (#2825) + +Fixed missing type casts when using `returning_one` / `returning_all`. + +**Temp fix MySQL JSON equals** + +Workaround for MySQL JSON equality comparison issues. + +**Automatically convert `eq(None)` to `IS NULL`** (#2826) + +`eq(None)` is now correctly translated to `IS NULL` in SQL. + +**Fix compile error when postgres-vector is enabled** (#2821) + +Resolved compilation failure with the `postgres-vector` feature. + +### Improvements + +- Entity Loader: move `EntityLoaderWithParam` trait to be owned by user for better blanket impl support +- Built with SeaORM: Add FirstLook.gg (#2828) diff --git a/changelog/2.0.0-rc.21.md b/changelog/2.0.0-rc.21.md new file mode 100644 index 0000000000..9a4fddef6d --- /dev/null +++ b/changelog/2.0.0-rc.21.md @@ -0,0 +1,31 @@ +## Release Notes: SeaORM 2.0.0-rc.21 + +*(since 2.0.0-rc.20)* + +### New Features + +**Rusqlite / sea-orm-sync** (#2847) + +- New `sea-orm-sync` crate for synchronous SQLite via rusqlite +- Stream and transaction support +- Integration with `sea-schema-sync` + +**Add `--banner-version` to CLI** (#2836) + +CLI now supports a `--banner-version` flag. + +**Add `exists` method to `PaginatorTrait`** (#2623) + +`PaginatorTrait` now includes an `exists` method for efficient existence checks. + +### Bug Fixes + +**Respect `#[serde(rename)]` in `ActiveModel::from_json`** (#2842) + +`ActiveModel::from_json` now correctly respects `#[serde(rename)]` and `#[serde(rename_all)]` attributes when deserializing JSON. + +### Improvements + +- Reduce Vec allocations (#2835) +- Add taplo for TOML formatting (#2844) +- SeaORM sync example and preparation for `sea-orm-sync` crate diff --git a/changelog/2.0.0-rc.22.md b/changelog/2.0.0-rc.22.md new file mode 100644 index 0000000000..40e57f62c9 --- /dev/null +++ b/changelog/2.0.0-rc.22.md @@ -0,0 +1,38 @@ +## Release Notes: SeaORM 2.0.0-rc.22 + +*(since 2.0.0-rc.21)* + +### New Features + +**`DatabaseExecutor` unified executor type** (#2839) + +Added a unified `DatabaseExecutor` type that wraps either a `&DatabaseConnection` or `&DatabaseTransaction`. This moves the `SchemaManagerConnection` pattern from `sea-orm-migration` into sea-orm core, providing: + +- `DatabaseExecutor` enum for unified handling of connections and transactions +- `IntoDatabaseExecutor` trait for ergonomic conversions +- `SchemaManagerConnection` type alias for backward compatibility +- `SchemaManagerConnectionExt` trait for migration-specific methods + +### Bug Fixes + +*None in this release.* + +### Improvements + +**Refactor Value array** (#2849) + +Refactored the internal `Value` array handling for improved consistency and maintainability. + +**Refactor CI** (#2859) + +- Parallelized test compilation for faster CI runs +- Updated `actions/cache` to v5 +- Fixed cache keys + +**sea-orm-sync example** + +Added and improved the `sea-orm-sync` example. + +**Update examples** + +Updated example code across the repository. diff --git a/changelog/2.0.0-rc.23.md b/changelog/2.0.0-rc.23.md new file mode 100644 index 0000000000..2c19d723fd --- /dev/null +++ b/changelog/2.0.0-rc.23.md @@ -0,0 +1,45 @@ +## Release Notes: SeaORM 2.0.0-rc.23 + +*(since 2.0.0-rc.22)* + +### New Features + +**`DeriveValueType` implements `IntoActiveValue`** (#2868) + +`DeriveValueType` now automatically implements `IntoActiveValue`, so custom value types can be used directly when building ActiveModels without additional boilerplate. + +**`IntoActiveValue` for more types** + +`IntoActiveValue` is now implemented for additional standard types, improving ergonomics when setting ActiveModel fields. + +### Bug Fixes + +**Fix sea-orm-sync** + +Fixed issues in the `sea-orm-sync` crate. + +**Fix sync** + +Fixed sync-related bugs. + +**Fix tests** + +Resolved failing tests. + +**Insert many on conflict do nothing returning** (#2389) + +Added test coverage and fixes for `insert_many` with `on_conflict_do_nothing` and `returning` clauses. + +### Improvements + +**Remove `NotU8` trait** (#2868) + +The `NotU8` trait has been removed. `DeriveValueType` now handles type constraints internally, simplifying the derive macro. + +**Remove `bigdecimal` from default features** + +The `bigdecimal` dependency is no longer included in default features, reducing the default dependency footprint. Enable it explicitly if needed. + +**CI improvements** + +Various CI fixes and improvements. diff --git a/changelog/2.0.0-rc.24.md b/changelog/2.0.0-rc.24.md new file mode 100644 index 0000000000..4b0ec7a1be --- /dev/null +++ b/changelog/2.0.0-rc.24.md @@ -0,0 +1,8 @@ +## Release Notes: SeaORM 2.0.0-rc.24 + +*(since 2.0.0-rc.23)* + +### Improvements + +- Bumped `sea-query` to 1.0.0-rc.27 +- Updated example projects to use the new release diff --git a/changelog/2.0.0-rc.25.md b/changelog/2.0.0-rc.25.md new file mode 100644 index 0000000000..db8ded2122 --- /dev/null +++ b/changelog/2.0.0-rc.25.md @@ -0,0 +1,18 @@ +## Release Notes: SeaORM 2.0.0-rc.25 + +*(since 2.0.0-rc.24)* + +### Bug Fixes + +**Restore value system** + +Restored the value system across the core crate, drivers (MySQL, PostgreSQL, SQLite), and entity/column types. Refactored executor and query handling for consistency. + +**Fix sea-orm-sync** + +Aligned `sea-orm-sync` with the restored value system changes in the main crate. Updated drivers, entity types, and executor/query logic to match. + +### Improvements + +- Bumped `sea-query` to 1.0.0-rc.28 +- Updated example projects to use the new release diff --git a/changelog/2.0.0-rc.26.md b/changelog/2.0.0-rc.26.md new file mode 100644 index 0000000000..2e7de02685 --- /dev/null +++ b/changelog/2.0.0-rc.26.md @@ -0,0 +1,9 @@ +## Release Notes: SeaORM 2.0.0-rc.26 + +*(since 2.0.0-rc.25)* + +### New Features + +**PostgreSQL legacy serial primary key option** + +Added the `postgres-use-serial-pk` feature flag to use legacy `serial` / `bigserial` types for auto-increment primary keys instead of `GENERATED BY DEFAULT AS IDENTITY`. Enable with `postgres-use-serial-pk` in your `Cargo.toml` if you need compatibility with older PostgreSQL setups or tooling. diff --git a/changelog/2.0.0-rc.27.md b/changelog/2.0.0-rc.27.md new file mode 100644 index 0000000000..065cd21e8d --- /dev/null +++ b/changelog/2.0.0-rc.27.md @@ -0,0 +1,9 @@ +## Release Notes: SeaORM 2.0.0-rc.27 + +*(since 2.0.0-rc.26)* + +### Improvements + +**DeriveValueType implements NotU8** + +`DeriveValueType` now also implements `sea_query::postgres_array::NotU8`, enabling custom value types to work with PostgreSQL array columns. This is auto-generated when the `postgres-array` feature is enabled. Remove any manual `NotU8` implementations on your custom types to avoid conflicts. diff --git a/changelog/2.0.0-rc.28.md b/changelog/2.0.0-rc.28.md new file mode 100644 index 0000000000..0566ac2f68 --- /dev/null +++ b/changelog/2.0.0-rc.28.md @@ -0,0 +1,37 @@ +## Release Notes: SeaORM 2.0.0-rc.28 + +*(since 2.0.0-rc.27)* + +### New Features + +**`sqlx-all` feature in sea-orm-migration** (#2887) + +Added `sqlx-all` feature flag to `sea-orm-migration` for compatibility with sqlx's all-database feature. + +**`set_if_not_equals_and` on ActiveValue** (#2888) + +Added `set_if_not_equals_and` method to `ActiveValue` for conditional updates based on value comparison. + +**Set auto_increment to false for String/Uuid primary keys by default** (#2881) + +Macros now set `auto_increment = false` by default when the primary key is `String` or `Uuid`, avoiding incorrect schema generation. + +### Bug Fixes + +**Postgres: combine SET TRANSACTION statements** (#2893) + +Combined `SET TRANSACTION ISOLATION LEVEL` and `ACCESS MODE` into a single statement for PostgreSQL, fixing transaction setup. + +### Improvements + +**Debug log for entity registry** (#2900) + +Added debug logging for entity registry operations to aid troubleshooting. + +**Deprecate do_nothing and on_empty_do_nothing** (#2883) + +Deprecated `do_nothing` and `on_empty_do_nothing` on insert operations. Added documentation and tests for the deprecation path. + +**Add Rustify to COMMUNITY.md** (#2898) + +Added Rustify bot to the community bots section, covering multi-source lyrics, AI analysis, real-time profanity detection, and auto-skip features. diff --git a/changelog/2.0.0-rc.29.md b/changelog/2.0.0-rc.29.md new file mode 100644 index 0000000000..a24fb07de8 --- /dev/null +++ b/changelog/2.0.0-rc.29.md @@ -0,0 +1,57 @@ +## Release Notes: SeaORM 2.0.0-rc.29 + +*(since 2.0.0-rc.28)* + +### New Features + +**Tracing spans for database operations** (#2885) + +Added `ObservabilityHook` and `with_db_span!` macro for distributed tracing support. Database operations and transactions emit tracing spans when the `tracing-spans` feature is enabled. + +**Wrapper type for storing UUIDs as TEXT** (#2914) + +Added a wrapper type for storing `Uuid` values as TEXT columns. + +**`Insert::try_insert` method** + +Added `try_insert` to the `Insert` trait for fallible insert operations. + +**Filter relations outside generation set** (#2913) + +Codegen now filters relations that fall outside the generation set. + +### Bug Fixes + +**Allow LEFT JOIN to produce None for nested models** (#2845) + +Fixed handling of nested `Option` in `FromQueryResult` when using LEFT JOINs, so optional nested models correctly produce `None` when no related row exists. + +**Fix DeriveIntoActiveModel** (#2926) + +Fixed `DeriveIntoActiveModel` derive macro; added test coverage. + +**Fix tracing spans visibility** (#2925) + +Corrected tracing span visibility for database operations. + +**Fix derive enums without per-case rename** (#2922) + +Fixed codegen for enums that do not use per-variant `rename` attributes. Addresses [#2921](https://github.com/SeaQL/sea-orm/issues/2921). + +**Fix renaming enum variants starting with digits** (#2905) + +Enum variants that start with digits are now correctly renamed during codegen. + +**Revert multi-argument extra attribute support** (#2915) + +Reverted the multi-argument extra attribute codegen change (#2560) due to incorrect test input parsing that caused `syn` to panic. The attribute format should split arguments by `,`. + +**Add missing lifetime hint to `EntityName::table_name`** (#2907) + +Added the missing lifetime hint to fix compilation in certain contexts. + +### Improvements + +**Optimize exists** (#2909) + +Improved performance of the `exists` query. diff --git a/changelog/2.0.0-rc.30.md b/changelog/2.0.0-rc.30.md new file mode 100644 index 0000000000..f90fc9085d --- /dev/null +++ b/changelog/2.0.0-rc.30.md @@ -0,0 +1,13 @@ +## Release Notes: SeaORM 2.0.0-rc.30 + +*(since 2.0.0-rc.29)* + +### Improvements + +**Dependency updates** + +Bumped `sea-query` dependency. + +**Examples** + +Updated examples. diff --git a/changelog/2.0.0-rc.31.md b/changelog/2.0.0-rc.31.md new file mode 100644 index 0000000000..4bec48c152 --- /dev/null +++ b/changelog/2.0.0-rc.31.md @@ -0,0 +1,45 @@ +## Release Notes: SeaORM 2.0.0-rc.31 + +*(since 2.0.0-rc.30)* + +### New Features + +**`ne_all` for `Condition`** + +Added `ne_all` to complement `eq_any`, allowing negation of multiple values in conditions. + +**Typed column for `TextUuid`** (#2717) + +Added typed column support for `TextUuid` type. + +**Custom derives for `Model` and `ModelEx`** (#2933) + +Macros now support `model_attrs` and `model_ex_attrs` to pass custom derives and attributes to the generated `Model` and `ModelEx` structs. Enables customization such as TypeScript interface names when using `ts-rs`: + +```rust +#[sea_orm::model] +#[derive(TS, ...)] +#[sea_orm(model_attrs(ts(rename = "Fruit")))] +#[sea_orm(model_ex_attrs(ts(rename = "FruitEx")))] +struct Model { ... } +``` + +### Bug Fixes + +**`COUNT(*)` overflow on MySQL and SQLite** (#2944) + +`COUNT(*)` now returns `i64` on all backends. MySQL's `COUNT(*)` returns `BIGINT` (64-bit); previously it was read as `i32`, which could overflow for large datasets. + +**Proxy error handling** (#2935) + +Fixed proxy error handling. + +### Improvements + +**Relaxed trait bounds for `eq_any` / `ne_all`** + +Relaxed trait bounds on `eq_any` and `ne_all` for broader compatibility. Depends on latest `sea-query`. + +**Raw string hashes** (#2381) + +Refactored to remove raw string hashes for improved readability. diff --git a/changelog/2.0.0-rc.32.md b/changelog/2.0.0-rc.32.md new file mode 100644 index 0000000000..dbebde8cba --- /dev/null +++ b/changelog/2.0.0-rc.32.md @@ -0,0 +1,17 @@ +## Release Notes: SeaORM 2.0.0-rc.32 + +*(since 2.0.0-rc.31)* + +### New Features + +**`MigratorTrait` with `self`** (#2806) + +`MigratorTrait` now uses `MigratorTraitSelf` with a shim, enabling migrator implementations that take `self` by reference. + +**PostgreSQL `application_name`** + +Added support for setting `application_name` when connecting to PostgreSQL. + +### Improvements + +- Updated examples diff --git a/changelog/2.0.0-rc.34.md b/changelog/2.0.0-rc.34.md new file mode 100644 index 0000000000..de3a749983 --- /dev/null +++ b/changelog/2.0.0-rc.34.md @@ -0,0 +1,45 @@ +## Release Notes: SeaORM 2.0.0-rc.34 + +*(since 2.0.0-rc.32)* + +### New Features + +**Arrow / Parquet support** (#2957) + +- Extracted `sea-orm-arrow` into a dedicated crate +- Added `ArrowSchema`, `DeriveArrowSchema` for entity-to-Arrow conversion +- Support decimal with different formats and timestamp with different timezone/resolution +- Added `ActiveModel::from_arrow` for creating ActiveModels from Arrow arrays +- Parquet example added +- Arrow support in `sea-orm-sync` + +**`DeriveValueType`: `try_from_u64`** (#2958) + +`DeriveValueType` now derives `TryFromU64` for custom value types. + +**Derive `Clone` for topologies** (#2954) + +Topology types can now derive `Clone`. + +**`preserve-user-modifications` experimental flag** (#2964) + +Added experimental `preserve-user-modifications` option for schema sync; default is `false`. + +**Optional `sea-schema`** + +`sea-schema` can be optional in some configurations. + +### Bug Fixes + +**Don't create redundant unique indexes** (#2950) + +Fixed creation of redundant unique indexes on PostgreSQL and SQLite when a column is already unique. Fixes #2873. + +**Typo: "exsiting" → "existing"** (#2956) + +### Improvements + +- Refactored `sea-orm-arrow`; upgraded to Arrow 57; removed direct Arrow dependency +- Improved docs for `DeriveValueType` (#2958) +- Split up test suite by creating only needed tables per test +- Support SeaORM X in `sea-orm-sync` diff --git a/changelog/2.0.0-rc.35.md b/changelog/2.0.0-rc.35.md new file mode 100644 index 0000000000..a449361af4 --- /dev/null +++ b/changelog/2.0.0-rc.35.md @@ -0,0 +1,159 @@ +## Release Notes: SeaORM 2.0.0-rc.35 + +*(since 2.0.0-rc.34)* + +### New Features + +**SQLite Transaction Modes** (#2932) + +Added `begin_with_options` to `TransactionTrait`, allowing you to specify SQLite transaction modes (`DEFERRED`, `IMMEDIATE`, `EXCLUSIVE`), along with isolation level and access mode for other backends. Works for both sqlx-sqlite and rusqlite. + +```rust +use sea_orm::{TransactionTrait, TransactionOptions, SqliteTransactionMode}; + +let txn = db.begin_with_options(TransactionOptions { + sqlite_transaction_mode: Some(SqliteTransactionMode::Immediate), + ..Default::default() +}).await?; +``` + +Nested transactions correctly fall back to `SAVEPOINT` regardless of the mode. + +**Extend `DeriveIntoActiveModel`** (#2961) + +`DeriveIntoActiveModel` now supports `set`, `default`, `ignore`, `exhaustive`, and custom `active_model` path attributes for fine-grained control when converting "form" or "input" structs into ActiveModels. + +**`set`** — always set fields not present on the struct: + +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "fruit::ActiveModel", set(cake_id = "None"))] +struct NewFruit { + name: String, + // cake_id is not on the struct, but will always be Set(None) +} + +NewFruit { name: "Apple".into() }.into_active_model() +// => ActiveModel { id: NotSet, name: Set("Apple"), cake_id: Set(None) } +``` + +Multiple `set` entries can be combined or split across attributes: + +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm( + active_model = "fruit::ActiveModel", + set(name = "String::from(\"cherry\")", cake_id = "None") +)] +struct IdOnlyFruit { + id: i32, +} + +IdOnlyFruit { id: 1 }.into_active_model() +// => ActiveModel { id: Set(1), name: Set("cherry"), cake_id: Set(None) } +``` + +**`default`** — fallback value when an `Option` field is `None`: + +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "fruit::ActiveModel")] +struct NewFruit { + #[sea_orm(default = "String::from(\"Unnamed\")")] + name: Option, +} + +NewFruit { name: Some("Apple".into()) }.into_active_model() +// => ActiveModel { id: NotSet, name: Set("Apple"), cake_id: NotSet } + +NewFruit { name: None }.into_active_model() +// => ActiveModel { id: NotSet, name: Set("Unnamed"), cake_id: NotSet } +``` + +Bare `#[sea_orm(default)]` (without a value) uses `Default::default()` as the fallback. This also works with custom enum types that implement `Into>`. + +**`ignore`** — exclude struct fields from the ActiveModel: + +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "fruit::ActiveModel")] +struct NewFruit { + name: String, + cake_id: i32, + #[sea_orm(ignore)] + _extra: String, // not mapped to ActiveModel +} +``` + +**`exhaustive`** — require all ActiveModel fields to be either on the struct or in `set(...)`: + +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "fruit::ActiveModel", exhaustive, set(cake_id = "None"))] +struct FullFruit { + id: i32, + name: String, + // cake_id is covered by set(...), so all fields are accounted for +} +``` + +**Combining everything** — `set` + `default` + `ignore` + `exhaustive`: + +```rust +#[derive(DeriveIntoActiveModel)] +#[sea_orm(active_model = "fruit::ActiveModel", exhaustive, set(cake_id = "None"))] +struct NewFruit { + id: i32, + #[sea_orm(default = "String::from(\"Unnamed\")")] + name: Option, +} + +NewFruit { id: 1, name: Some("Apple".into()) }.into_active_model() +// => ActiveModel { id: Set(1), name: Set("Apple"), cake_id: Set(None) } + +NewFruit { id: 2, name: None }.into_active_model() +// => ActiveModel { id: Set(2), name: Set("Unnamed"), cake_id: Set(None) } +``` + +**`IntoSimpleExpr` for `FunctionCall`** (#2822) + +`FunctionCall` now implements `IntoSimpleExpr`, so function calls can be used directly in select expressions and filters without wrapping in `SimpleExpr`. + +**Arrow: Support `Decimal64` and Fixed-Size Binary** (#2957) + +- Decimal columns with precision <= 18 now map to Arrow `Decimal64` (was always `Decimal128`) +- Precision 19-38 maps to `Decimal128`, above 38 to `Decimal256` +- Added `FixedSizeBinary(N)` support via `#[sea_orm(arrow_byte_width = N)]` +- Added `BinaryArray` / `LargeBinaryArray` / `FixedSizeBinaryArray` to `Value::Bytes` conversion + +**Optional `time` crate for Migrations** (#2865) + +Migrations can now use the `time` crate instead of `std::time::SystemTime` for timestamps, enabling compilation to WASM targets. Activate with the `with-time` feature on `sea-orm-migration`. + +**OpenTelemetry `SpanKind::Client`** (#2937) + +The `db_span!` macro now emits `otel.kind = "client"`, ensuring database spans are properly recognized as client spans by APM tools (Datadog, Jaeger, etc.). + +### Bug Fixes + +**Fix unique column in schema sync** (#2971) + +Columns marked with `#[sea_orm(unique)]` are now correctly handled by the schema sync/diff builder, generating proper unique constraints instead of being silently ignored. + +**Fix `DeriveArrowSchema` with split attributes** (#2973) + +Fixed a compilation error when `#[sea_orm(...)]` attributes were split across multiple lines on the same field (e.g. `#[sea_orm(primary_key)]` and `#[sea_orm(auto_increment = false)]` separately). The macro now properly consumes attributes it doesn't recognize. + +**Map internal error types properly** + +Internal errors from the schema builder are now mapped to the correct `DbErr` variants instead of being lost or mistyped. + +### Improvements + +**Pi Spigot Example** + +The `sea-orm-sync` pi spigot example has been refactored into a tutorial-style example with: +- OOP `PiSpigot` struct with state machine pattern (`new` / `step` / `finalize` / `to_state` / `from_state`) +- `clap` CLI with `--digits`, `--checkpoint`, and `--db` flags +- Comprehensive tests against 1000 known digits of pi, including three-phase checkpoint/resume +- Tutorial README demonstrating how to add SQLite persistence to any program diff --git a/changelog/2.0.0-rc.36.md b/changelog/2.0.0-rc.36.md new file mode 100644 index 0000000000..aa0be396c5 --- /dev/null +++ b/changelog/2.0.0-rc.36.md @@ -0,0 +1,51 @@ +## Release Notes: SeaORM 2.0.0-rc.36 + +*(since 2.0.0-rc.35)* + +### New Features + +**Per-Migration Transaction Control** (#2980) + +Previously, all Postgres migrations ran inside a single batch transaction, while MySQL and SQLite ran without one. This was an all-or-nothing approach with no way to opt out for individual migrations (e.g. `CREATE INDEX CONCURRENTLY` on Postgres requires running outside a transaction). + +`MigrationTrait` now has a `use_transaction()` method to control this per migration: + +```rust +impl MigrationTrait for Migration { + fn use_transaction(&self) -> Option { + Some(false) // opt out of automatic transaction + } +} +``` + +- `None` (default): follow backend convention — Postgres uses a transaction, MySQL/SQLite do not +- `Some(true)`: force a transaction on any backend +- `Some(false)`: disable automatic transaction wrapping + +For migrations that opt out, `SchemaManager::begin()` and `SchemaManager::commit()` allow manual transaction control within the migration body: + +```rust +async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // DDL in a transaction + let m = manager.begin().await?; + m.create_table( + Table::create() + .table("my_table") + .col(pk_auto("id")) + .col(string("name")) + .to_owned(), + ).await?; + m.commit().await?; + + // Non-transactional DDL + manager.get_connection() + .execute_unprepared("CREATE INDEX CONCURRENTLY idx_name ON my_table (name)") + .await?; + Ok(()) +} +``` + +Core changes: +- Added `OwnedTransaction` variant to `DatabaseExecutor`, enabling `SchemaManager` to own a transaction +- Added `DatabaseExecutor::is_transaction()` for runtime introspection +- Each migration is now wrapped individually rather than in a batch diff --git a/changelog/2.0.0-rc.37.md b/changelog/2.0.0-rc.37.md new file mode 100644 index 0000000000..e6c2b11116 --- /dev/null +++ b/changelog/2.0.0-rc.37.md @@ -0,0 +1,71 @@ +## Release Notes: SeaORM 2.0.0-rc.37 + +*(since 2.0.0-rc.36)* + +### New Features + +**ER Diagram Generation** (`sea-orm-cli generate entity --er-diagram`) + +`sea-orm-cli` can now generate a [Mermaid](https://mermaid.js.org/) ER diagram alongside the entity files. Pass `--er-diagram` to write `entities.mermaid` into the output directory: + +```sh +sea-orm-cli generate entity -u postgres://... -o src/entity --er-diagram +``` + +The diagram annotates columns with `PK`, `FK`, and `UK` markers and renders all relations — including many-to-many via junction tables — as Mermaid `erDiagram` syntax. Example output: + +``` +erDiagram + user { + int id PK + varchar name + varchar email UK + int parent_id FK + } + post { + int id PK + text title + int user_id FK + } + user }o--|| user : "parent_id" + post }o--|| user : "user_id" + post }o--o{ tag : "[post_tag]" +``` + +**PostgreSQL Statement Timeout** (`ConnectOptions::statement_timeout`) + +`ConnectOptions` now accepts a `statement_timeout` for PostgreSQL connections. The timeout is set via the connection options at connect time (no extra round-trip) and causes the server to abort any statement that exceeds the duration: + +```rust +ConnectOptions::new(DATABASE_URL) + .statement_timeout(Duration::from_secs(30)) + .to_owned() +``` + +Has no effect on MySQL or SQLite connections. + +**SQLite `?mode=` URL Parameter Support** (#2987) + +The rusqlite driver now parses the `?mode=` query parameter from SQLite connection URLs, matching the behaviour of the sqlx SQLite driver: + +| Mode | Behaviour | +|---|---| +| `rwc` (default) | Read-write, create if not exists | +| `rw` | Read-write, must exist | +| `ro` | Read-only | +| `memory` | In-memory database | + +```rust +// Open an existing database read-only +let db = Database::connect("sqlite:./data.db?mode=ro").await?; +``` + +Unsupported parameters or unknown mode values return a `DbErr::Conn` error. + +### Bug Fixes + +**`no-default-features` compile errors with `mac_address` and `proxy`** (#2992) + +- `with-mac_address` feature: added missing `TryGetable` impls, `try_from_u64` impl, postgres array support, and `with-json` serde flag +- `proxy` feature: removed an accidental hard dependency on `serde_json` (now only activated via `with-json`) +- Fixed `cfg` guards on JSON/JSONB proxy row handling to require `with-json` diff --git a/docs/sea-orm-pro-dark.png b/docs/sea-orm-pro-dark.png new file mode 100644 index 0000000000..8e03bc246b Binary files /dev/null and b/docs/sea-orm-pro-dark.png differ diff --git a/docs/sea-orm-pro-light.png b/docs/sea-orm-pro-light.png new file mode 100644 index 0000000000..21b5d9c0b9 Binary files /dev/null and b/docs/sea-orm-pro-light.png differ diff --git a/examples/actix3_example/.env b/examples/actix3_example/.env deleted file mode 100644 index 1a12a183ab..0000000000 --- a/examples/actix3_example/.env +++ /dev/null @@ -1,3 +0,0 @@ -HOST=127.0.0.1 -PORT=8000 -DATABASE_URL="mysql://root:root@localhost/actix_example" \ No newline at end of file diff --git a/examples/actix3_example/Cargo.toml b/examples/actix3_example/Cargo.toml deleted file mode 100644 index a449de4ca4..0000000000 --- a/examples/actix3_example/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "sea-orm-actix-example" -version = "0.1.0" -authors = ["Sam Samai "] -edition = "2021" -publish = false - -[workspace] -members = [".", "api", "core", "entity", "migration"] - -[dependencies] -actix3-example-api = { path = "api" } diff --git a/examples/actix3_example/README.md b/examples/actix3_example/README.md deleted file mode 100644 index 4726382613..0000000000 --- a/examples/actix3_example/README.md +++ /dev/null @@ -1,27 +0,0 @@ -![screenshot](Screenshot.png) - -# Actix 3 with SeaORM example app - -> Actix Web 3 has been superseeded by [Actix Web 4](https://github.com/actix/actix-web). - -1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database - -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-mysql",` line) - -1. Execute `cargo run` to start the server - -1. Visit [localhost:8000](http://localhost:8000) in browser - -Run server with auto-reloading: - -```bash -cargo install systemfd -systemfd --no-pid -s http::8000 -- cargo watch -x run -``` - -Run mock test on the core logic crate: - -```bash -cd core -cargo test --features mock -``` diff --git a/examples/actix3_example/Screenshot.png b/examples/actix3_example/Screenshot.png deleted file mode 100644 index 8846f70fd2..0000000000 Binary files a/examples/actix3_example/Screenshot.png and /dev/null differ diff --git a/examples/actix3_example/api/Cargo.toml b/examples/actix3_example/api/Cargo.toml deleted file mode 100644 index 63540ec988..0000000000 --- a/examples/actix3_example/api/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "actix3-example-api" -version = "0.1.0" -authors = ["Sam Samai "] -edition = "2021" -publish = false - -[dependencies] -actix3-example-core = { path = "../core" } -actix-http = "2" -actix-web = "3" -actix-flash = "0.2" -actix-files = "0.5" -futures = { version = "0.3" } -futures-util = { version = "0.3" } -tera = "1.8.0" -dotenvy = "0.15" -listenfd = "0.3.3" -serde = "1" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -entity = { path = "../entity" } -migration = { path = "../migration" } diff --git a/examples/actix3_example/api/src/lib.rs b/examples/actix3_example/api/src/lib.rs deleted file mode 100644 index 32f6f5f5ac..0000000000 --- a/examples/actix3_example/api/src/lib.rs +++ /dev/null @@ -1,219 +0,0 @@ -use actix3_example_core::{ - sea_orm::{Database, DatabaseConnection}, - Mutation, Query, -}; -use actix_files as fs; -use actix_web::{ - error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, -}; - -use entity::post; -use listenfd::ListenFd; -use migration::{Migrator, MigratorTrait}; -use serde::{Deserialize, Serialize}; -use std::env; -use tera::Tera; - -const DEFAULT_POSTS_PER_PAGE: u64 = 5; - -#[derive(Debug, Clone)] -struct AppState { - templates: tera::Tera, - conn: DatabaseConnection, -} -#[derive(Debug, Deserialize)] -pub struct Params { - page: Option, - posts_per_page: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct FlashData { - kind: String, - message: String, -} - -#[get("/")] -async fn list( - req: HttpRequest, - data: web::Data, - opt_flash: Option>, -) -> Result { - let template = &data.templates; - let conn = &data.conn; - - // get params - let params = web::Query::::from_query(req.query_string()).unwrap(); - - let page = params.page.unwrap_or(1); - let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - - let (posts, num_pages) = Query::find_posts_in_page(conn, page, posts_per_page) - .await - .expect("Cannot find posts in page"); - - let mut ctx = tera::Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - if let Some(flash) = opt_flash { - let flash_inner = flash.into_inner(); - ctx.insert("flash", &flash_inner); - } - - let body = template - .render("index.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[get("/new")] -async fn new(data: web::Data) -> Result { - let template = &data.templates; - let ctx = tera::Context::new(); - let body = template - .render("new.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[post("/")] -async fn create( - data: web::Data, - post_form: web::Form, -) -> actix_flash::Response { - let conn = &data.conn; - - let form = post_form.into_inner(); - - Mutation::create_post(conn, form) - .await - .expect("could not insert post"); - - let flash = FlashData { - kind: "success".to_owned(), - message: "Post successfully added.".to_owned(), - }; - - actix_flash::Response::with_redirect(flash, "/") -} - -#[get("/{id}")] -async fn edit(data: web::Data, id: web::Path) -> Result { - let conn = &data.conn; - let template = &data.templates; - let id = id.into_inner(); - - let post: post::Model = Query::find_post_by_id(conn, id) - .await - .expect("could not find post") - .unwrap_or_else(|| panic!("could not find post with id {id}")); - - let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = template - .render("edit.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[post("/{id}")] -async fn update( - data: web::Data, - id: web::Path, - post_form: web::Form, -) -> actix_flash::Response { - let conn = &data.conn; - let form = post_form.into_inner(); - let id = id.into_inner(); - - Mutation::update_post_by_id(conn, id, form) - .await - .expect("could not edit post"); - - let flash = FlashData { - kind: "success".to_owned(), - message: "Post successfully updated.".to_owned(), - }; - - actix_flash::Response::with_redirect(flash, "/") -} - -#[post("/delete/{id}")] -async fn delete( - data: web::Data, - id: web::Path, -) -> actix_flash::Response { - let conn = &data.conn; - let id = id.into_inner(); - - Mutation::delete_post(conn, id) - .await - .expect("could not delete post"); - - let flash = FlashData { - kind: "success".to_owned(), - message: "Post successfully deleted.".to_owned(), - }; - - actix_flash::Response::with_redirect(flash, "/") -} - -#[actix_web::main] -async fn start() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "debug"); - tracing_subscriber::fmt::init(); - - // get env vars - dotenvy::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{host}:{port}"); - - // create post table if not exists - let conn = Database::connect(&db_url).await.unwrap(); - Migrator::up(&conn, None).await.unwrap(); - let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); - let state = AppState { templates, conn }; - - let mut listenfd = ListenFd::from_env(); - let mut server = HttpServer::new(move || { - App::new() - .data(state.clone()) - .wrap(middleware::Logger::default()) // enable logger - .wrap(actix_flash::Flash::default()) - .configure(init) - .service(fs::Files::new("/static", "./api/static").show_files_listing()) - }); - - server = match listenfd.take_tcp_listener(0)? { - Some(listener) => server.listen(listener)?, - None => server.bind(&server_url)?, - }; - - println!("Starting server at {server_url}"); - server.run().await?; - - Ok(()) -} - -fn init(cfg: &mut web::ServiceConfig) { - cfg.service(list); - cfg.service(new); - cfg.service(create); - cfg.service(edit); - cfg.service(update); - cfg.service(delete); -} - -pub fn main() { - let result = start(); - - if let Some(err) = result.err() { - println!("Error: {err}") - } -} diff --git a/examples/actix3_example/api/static/css/normalize.css b/examples/actix3_example/api/static/css/normalize.css deleted file mode 100644 index 458eea1ea3..0000000000 --- a/examples/actix3_example/api/static/css/normalize.css +++ /dev/null @@ -1,427 +0,0 @@ -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/examples/actix3_example/api/static/css/skeleton.css b/examples/actix3_example/api/static/css/skeleton.css deleted file mode 100644 index cdc432a47f..0000000000 --- a/examples/actix3_example/api/static/css/skeleton.css +++ /dev/null @@ -1,421 +0,0 @@ -/* -* Skeleton V2.0.4 -* Copyright 2014, Dave Gamache -* www.getskeleton.com -* Free to use under the MIT license. -* https://opensource.org/licenses/mit-license.php -* 12/29/2014 -*/ - - -/* Table of contents -–––––––––––––––––––––––––––––––––––––––––––––––––– -- Grid -- Base Styles -- Typography -- Links -- Buttons -- Forms -- Lists -- Code -- Tables -- Spacing -- Utilities -- Clearing -- Media Queries -*/ - - -/* Grid -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; } -.column, -.columns { - width: 100%; - float: left; - box-sizing: border-box; } - -/* For devices larger than 400px */ -@media (min-width: 400px) { - .container { - width: 85%; - padding: 0; } -} - -/* For devices larger than 550px */ -@media (min-width: 550px) { - .container { - width: 80%; } - .column, - .columns { - margin-left: 4%; } - .column:first-child, - .columns:first-child { - margin-left: 0; } - - .one.column, - .one.columns { width: 4.66666666667%; } - .two.columns { width: 13.3333333333%; } - .three.columns { width: 22%; } - .four.columns { width: 30.6666666667%; } - .five.columns { width: 39.3333333333%; } - .six.columns { width: 48%; } - .seven.columns { width: 56.6666666667%; } - .eight.columns { width: 65.3333333333%; } - .nine.columns { width: 74.0%; } - .ten.columns { width: 82.6666666667%; } - .eleven.columns { width: 91.3333333333%; } - .twelve.columns { width: 100%; margin-left: 0; } - - .one-third.column { width: 30.6666666667%; } - .two-thirds.column { width: 65.3333333333%; } - - .one-half.column { width: 48%; } - - /* Offsets */ - .offset-by-one.column, - .offset-by-one.columns { margin-left: 8.66666666667%; } - .offset-by-two.column, - .offset-by-two.columns { margin-left: 17.3333333333%; } - .offset-by-three.column, - .offset-by-three.columns { margin-left: 26%; } - .offset-by-four.column, - .offset-by-four.columns { margin-left: 34.6666666667%; } - .offset-by-five.column, - .offset-by-five.columns { margin-left: 43.3333333333%; } - .offset-by-six.column, - .offset-by-six.columns { margin-left: 52%; } - .offset-by-seven.column, - .offset-by-seven.columns { margin-left: 60.6666666667%; } - .offset-by-eight.column, - .offset-by-eight.columns { margin-left: 69.3333333333%; } - .offset-by-nine.column, - .offset-by-nine.columns { margin-left: 78.0%; } - .offset-by-ten.column, - .offset-by-ten.columns { margin-left: 86.6666666667%; } - .offset-by-eleven.column, - .offset-by-eleven.columns { margin-left: 95.3333333333%; } - - .offset-by-one-third.column, - .offset-by-one-third.columns { margin-left: 34.6666666667%; } - .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } - - .offset-by-one-half.column, - .offset-by-one-half.columns { margin-left: 52%; } - -} - - -/* Base Styles -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* NOTE -html is set to 62.5% so that all the REM measurements throughout Skeleton -are based on 10px sizing. So basically 1.5rem = 15px :) */ -html { - font-size: 62.5%; } -body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #222; } - - -/* Typography -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, h2, h3, h4, h5, h6 { - margin-top: 0; - margin-bottom: 2rem; - font-weight: 300; } -h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} -h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } -h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } -h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } -h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } -h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } - -/* Larger than phablet */ -@media (min-width: 550px) { - h1 { font-size: 5.0rem; } - h2 { font-size: 4.2rem; } - h3 { font-size: 3.6rem; } - h4 { font-size: 3.0rem; } - h5 { font-size: 2.4rem; } - h6 { font-size: 1.5rem; } -} - -p { - margin-top: 0; } - - -/* Links -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -a { - color: #1EAEDB; } -a:hover { - color: #0FA0CE; } - - -/* Buttons -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.button, -button, -input[type="submit"], -input[type="reset"], -input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; } -.button:hover, -button:hover, -input[type="submit"]:hover, -input[type="reset"]:hover, -input[type="button"]:hover, -.button:focus, -button:focus, -input[type="submit"]:focus, -input[type="reset"]:focus, -input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; } -.button.button-primary, -button.button-primary, -button.primary, -input[type="submit"].button-primary, -input[type="reset"].button-primary, -input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; } -.button.button-primary:hover, -button.button-primary:hover, -button.primary:hover, -input[type="submit"].button-primary:hover, -input[type="reset"].button-primary:hover, -input[type="button"].button-primary:hover, -.button.button-primary:focus, -button.button-primary:focus, -button.primary:focus, -input[type="submit"].button-primary:focus, -input[type="reset"].button-primary:focus, -input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; } - - -/* Forms -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea, -select { - height: 38px; - padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; } -/* Removes awkward default styles on some inputs for iOS */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } -textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; } -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="text"]:focus, -input[type="tel"]:focus, -input[type="url"]:focus, -input[type="password"]:focus, -textarea:focus, -select:focus { - border: 1px solid #33C3F0; - outline: 0; } -label, -legend { - display: block; - margin-bottom: .5rem; - font-weight: 600; } -fieldset { - padding: 0; - border-width: 0; } -input[type="checkbox"], -input[type="radio"] { - display: inline; } -label > .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; } - - -/* Lists -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -ul { - list-style: circle inside; } -ol { - list-style: decimal inside; } -ol, ul { - padding-left: 0; - margin-top: 0; } -ul ul, -ul ol, -ol ol, -ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } -li { - margin-bottom: 1rem; } - - -/* Code -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -code { - padding: .2rem .5rem; - margin: 0 .2rem; - font-size: 90%; - white-space: nowrap; - background: #F1F1F1; - border: 1px solid #E1E1E1; - border-radius: 4px; } -pre > code { - display: block; - padding: 1rem 1.5rem; - white-space: pre; } - - -/* Tables -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -th, -td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; } -th:first-child, -td:first-child { - padding-left: 0; } -th:last-child, -td:last-child { - padding-right: 0; } - - -/* Spacing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -button, -.button { - margin-bottom: 1rem; } -input, -textarea, -select, -fieldset { - margin-bottom: 1.5rem; } -pre, -blockquote, -dl, -figure, -table, -p, -ul, -ol, -form { - margin-bottom: 2.5rem; } - - -/* Utilities -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.u-full-width { - width: 100%; - box-sizing: border-box; } -.u-max-full-width { - max-width: 100%; - box-sizing: border-box; } -.u-pull-right { - float: right; } -.u-pull-left { - float: left; } - - -/* Misc -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; } - - -/* Clearing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ - -/* Self Clearing Goodness */ -.container:after, -.row:after, -.u-cf { - content: ""; - display: table; - clear: both; } - - -/* Media Queries -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* -Note: The best way to structure the use of media queries is to create the queries -near the relevant code. For example, if you wanted to change the styles for buttons -on small devices, paste the mobile query code up in the buttons section and style it -there. -*/ - - -/* Larger than mobile */ -@media (min-width: 400px) {} - -/* Larger than phablet (also point when grid becomes active) */ -@media (min-width: 550px) {} - -/* Larger than tablet */ -@media (min-width: 750px) {} - -/* Larger than desktop */ -@media (min-width: 1000px) {} - -/* Larger than Desktop HD */ -@media (min-width: 1200px) {} diff --git a/examples/actix3_example/api/static/css/style.css b/examples/actix3_example/api/static/css/style.css deleted file mode 100644 index ac2720d357..0000000000 --- a/examples/actix3_example/api/static/css/style.css +++ /dev/null @@ -1,73 +0,0 @@ -.field-error { - border: 1px solid #ff0000 !important; -} - -.field-error-flash { - color: #ff0000; - display: block; - margin: -10px 0 10px 0; -} - -.field-success { - border: 1px solid #5ab953 !important; -} - -.field-success-flash { - color: #5ab953; - display: block; - margin: -10px 0 10px 0; -} - -span.completed { - text-decoration: line-through; -} - -form.inline { - display: inline; -} - -form.link, -button.link { - display: inline; - color: #1eaedb; - border: none; - outline: none; - background: none; - cursor: pointer; - padding: 0; - margin: 0 0 0 0; - height: inherit; - text-decoration: underline; - font-size: inherit; - text-transform: none; - font-weight: normal; - line-height: inherit; - letter-spacing: inherit; -} - -form.link:hover, -button.link:hover { - color: #0fa0ce; -} - -button.small { - height: 20px; - padding: 0 10px; - font-size: 10px; - line-height: 20px; - margin: 0 2.5px; -} - -.post:hover { - background-color: #bce2ee; -} - -.post td { - padding: 5px; - width: 150px; -} - -#delete-button { - color: red; - border-color: red; -} diff --git a/examples/actix3_example/api/static/images/favicon.png b/examples/actix3_example/api/static/images/favicon.png deleted file mode 100644 index 02b7390443..0000000000 Binary files a/examples/actix3_example/api/static/images/favicon.png and /dev/null differ diff --git a/examples/actix3_example/api/templates/edit.html.tera b/examples/actix3_example/api/templates/edit.html.tera deleted file mode 100644 index 5aeb22afc0..0000000000 --- a/examples/actix3_example/api/templates/edit.html.tera +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "layout.html.tera" %} {% block content %} -
-

Edit Post

-
-
-
-
- - -
-
-
- - - -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-{% endblock content %} diff --git a/examples/actix3_example/api/templates/error/404.html.tera b/examples/actix3_example/api/templates/error/404.html.tera deleted file mode 100644 index afda653d20..0000000000 --- a/examples/actix3_example/api/templates/error/404.html.tera +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 404 - tera - - -

404: Hey! There's nothing here.

- The page at {{ uri }} does not exist! - - diff --git a/examples/actix3_example/api/templates/index.html.tera b/examples/actix3_example/api/templates/index.html.tera deleted file mode 100644 index 1dccdeba59..0000000000 --- a/examples/actix3_example/api/templates/index.html.tera +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "layout.html.tera" %} {% block content %} -
-

-

Posts

- {% if flash %} - - {{ flash.message }} - - {% endif %} - - - - - - - - - - {% for post in posts %} - - - - - - {% endfor %} - - - - - - - - -
IDTitleText
{{ post.id }}{{ post.title }}{{ post.text }}
- {% if page == 1 %} Previous {% else %} - Previous - {% endif %} | {% if page == num_pages %} Next {% else %} - Next - {% endif %} -
- -
- - - -
-
-{% endblock content %} diff --git a/examples/actix3_example/api/templates/layout.html.tera b/examples/actix3_example/api/templates/layout.html.tera deleted file mode 100644 index c930afcca4..0000000000 --- a/examples/actix3_example/api/templates/layout.html.tera +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Actix Example - - - - - - - - - - - -
-

- {% block content %}{% endblock content %} -
- - diff --git a/examples/actix3_example/api/templates/new.html.tera b/examples/actix3_example/api/templates/new.html.tera deleted file mode 100644 index dee1956581..0000000000 --- a/examples/actix3_example/api/templates/new.html.tera +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "layout.html.tera" %} {% block content %} -
-

New Post

-
-
- - -
-
-
- - - -
-
-
- -
-
-
-
-{% endblock content %} diff --git a/examples/actix3_example/core/Cargo.toml b/examples/actix3_example/core/Cargo.toml deleted file mode 100644 index 53277a72e7..0000000000 --- a/examples/actix3_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "actix3-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-async-std-native-tls", - "sqlx-mysql", - # "sqlx-postgres", - # "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/actix3_example/core/tests/mock.rs b/examples/actix3_example/core/tests/mock.rs deleted file mode 100644 index 190cb29064..0000000000 --- a/examples/actix3_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use actix3_example_core::{Mutation, Query}; -use entity::post; -use prepare::prepare_mock_db; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/actix3_example/core/tests/prepare.rs b/examples/actix3_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/actix3_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/actix3_example/entity/Cargo.toml b/examples/actix3_example/entity/Cargo.toml deleted file mode 100644 index 7dea367337..0000000000 --- a/examples/actix3_example/entity/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -name = "entity" -path = "src/lib.rs" - -[dependencies] -serde = { version = "1", features = ["derive"] } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version diff --git a/examples/actix3_example/migration/Cargo.toml b/examples/actix3_example/migration/Cargo.toml deleted file mode 100644 index 1078662187..0000000000 --- a/examples/actix3_example/migration/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -name = "migration" -path = "src/lib.rs" - -[dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } - -[dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version -features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-async-std-native-tls", - # "sqlx-mysql", -] diff --git a/examples/actix3_example/migration/src/m20220120_000001_create_post_table.rs b/examples/actix3_example/migration/src/m20220120_000001_create_post_table.rs deleted file mode 100644 index a2fa0219cd..0000000000 --- a/examples/actix3_example/migration/src/m20220120_000001_create_post_table.rs +++ /dev/null @@ -1,42 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Posts::Table) - .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) - .await - } -} - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/actix3_example/src/main.rs b/examples/actix3_example/src/main.rs deleted file mode 100644 index 5a0e63b822..0000000000 --- a/examples/actix3_example/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - actix3_example_api::main(); -} diff --git a/examples/actix_example/Cargo.toml b/examples/actix_example/Cargo.toml index 7dc877c2c6..3fb8a62b61 100644 --- a/examples/actix_example/Cargo.toml +++ b/examples/actix_example/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "sea-orm-actix-4-beta-example" -version = "0.1.0" -authors = ["Sam Samai "] -edition = "2021" -publish = false +authors = ["Sam Samai "] +edition = "2024" +name = "sea-orm-actix-4-beta-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] actix-example-api = { path = "api" } diff --git a/examples/actix_example/README.md b/examples/actix_example/README.md index d844b96a24..c6ba87dc64 100644 --- a/examples/actix_example/README.md +++ b/examples/actix_example/README.md @@ -4,7 +4,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-mysql",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-mysql",` line) 1. Execute `cargo run` to start the server @@ -17,9 +17,21 @@ cargo install systemfd cargo-watch systemfd --no-pid -s http::8000 -- cargo watch -x run ``` -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock +cd api +cargo test ``` + +Run migration: + +```bash +cargo run -p migration -- up +``` + +Regenerate entity: + +```bash +sea-orm-cli generate entity --output-dir ./entity/src --lib --entity-format dense --with-serde both +``` \ No newline at end of file diff --git a/examples/actix_example/api/Cargo.toml b/examples/actix_example/api/Cargo.toml index a552466704..6fc7f59d07 100644 --- a/examples/actix_example/api/Cargo.toml +++ b/examples/actix_example/api/Cargo.toml @@ -1,21 +1,39 @@ [package] -name = "actix-example-api" -version = "0.1.0" -authors = ["Sam Samai "] -edition = "2021" -publish = false +authors = ["Sam Samai "] +edition = "2024" +name = "actix-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -actix-example-core = { path = "../core" } -actix-files = "0.6" -actix-http = "3" -actix-rt = "2.7" -actix-service = "2" -actix-web = "4" -tera = "1.15.0" -dotenvy = "0.15" -listenfd = "0.5" -serde = "1" +actix-files = "0.6" +actix-http = "3" +actix-rt = "2.8" +actix-service = "2" +actix-web = "4" +dotenvy = "0.15" +entity = { path = "../entity" } +listenfd = "1" +migration = { path = "../migration" } +serde = "1" +tera = "1.19.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -entity = { path = "../entity" } -migration = { path = "../migration" } + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-mysql", + # "sqlx-postgres", + # "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version + +[dev-dependencies] +actix-example-api = { path = ".", features = ["sqlite"] } +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +sqlite = ["sea-orm/sqlx-sqlite"] diff --git a/examples/actix_example/api/src/lib.rs b/examples/actix_example/api/src/lib.rs index 83cfa17f5b..f1b0708e2b 100644 --- a/examples/actix_example/api/src/lib.rs +++ b/examples/actix_example/api/src/lib.rs @@ -1,16 +1,16 @@ -use actix_example_core::{ - sea_orm::{Database, DatabaseConnection}, - Mutation, Query, -}; +pub mod service; + use actix_files::Files as Fs; use actix_web::{ - error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, + App, Error, HttpRequest, HttpResponse, HttpServer, Result, error, get, middleware, post, web, }; use entity::post; use listenfd::ListenFd; use migration::{Migrator, MigratorTrait}; +use sea_orm::{Database, DatabaseConnection}; use serde::{Deserialize, Serialize}; +use service::{Mutation, Query}; use std::env; use tera::Tera; @@ -89,24 +89,34 @@ async fn create( .finish()) } -#[get("/{id}")] +#[get(r#"/{id:\d+}"#)] async fn edit(data: web::Data, id: web::Path) -> Result { let conn = &data.conn; let template = &data.templates; let id = id.into_inner(); - let post: post::Model = Query::find_post_by_id(conn, id) + let post: Option = Query::find_post_by_id(conn, id) .await - .expect("could not find post") - .unwrap_or_else(|| panic!("could not find post with id {id}")); + .expect("could not find post"); let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = template - .render("edit.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) + let body = match post { + Some(post) => { + ctx.insert("post", &post); + + template + .render("edit.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error")) + } + None => { + ctx.insert("uri", &format!("/{}", id)); + + template + .render("error/404.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error")) + } + }; + Ok(HttpResponse::Ok().content_type("text/html").body(body?)) } #[post("/{id}")] @@ -156,7 +166,9 @@ async fn not_found(data: web::Data, request: HttpRequest) -> Result std::io::Result<()> { - std::env::set_var("RUST_LOG", "debug"); + unsafe { + std::env::set_var("RUST_LOG", "debug"); + } tracing_subscriber::fmt::init(); // get env vars diff --git a/examples/jsonrpsee_example/core/src/lib.rs b/examples/actix_example/api/src/service/mod.rs similarity index 78% rename from examples/jsonrpsee_example/core/src/lib.rs rename to examples/actix_example/api/src/service/mod.rs index 4a80f23913..4d0b4b4b4e 100644 --- a/examples/jsonrpsee_example/core/src/lib.rs +++ b/examples/actix_example/api/src/service/mod.rs @@ -3,5 +3,3 @@ mod query; pub use mutation::*; pub use query::*; - -pub use sea_orm; diff --git a/examples/actix3_example/core/src/mutation.rs b/examples/actix_example/api/src/service/mutation.rs similarity index 100% rename from examples/actix3_example/core/src/mutation.rs rename to examples/actix_example/api/src/service/mutation.rs diff --git a/examples/actix3_example/core/src/query.rs b/examples/actix_example/api/src/service/query.rs similarity index 100% rename from examples/actix3_example/core/src/query.rs rename to examples/actix_example/api/src/service/query.rs diff --git a/examples/actix_example/api/tests/crud_tests.rs b/examples/actix_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..f6f19cd446 --- /dev/null +++ b/examples/actix_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use actix_example_api::service::{Mutation, Query}; +use entity::post; +use sea_orm::Database; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/actix_example/core/Cargo.toml b/examples/actix_example/core/Cargo.toml deleted file mode 100644 index b9d4927666..0000000000 --- a/examples/actix_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "actix-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-async-std-native-tls", - "sqlx-mysql", - # "sqlx-postgres", - # "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/actix_example/core/tests/mock.rs b/examples/actix_example/core/tests/mock.rs deleted file mode 100644 index 76531b6710..0000000000 --- a/examples/actix_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use actix_example_core::{Mutation, Query}; -use entity::post; -use prepare::prepare_mock_db; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/actix_example/core/tests/prepare.rs b/examples/actix_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/actix_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/actix_example/entity/Cargo.toml b/examples/actix_example/entity/Cargo.toml index 7dea367337..9e429a350b 100644 --- a/examples/actix_example/entity/Cargo.toml +++ b/examples/actix_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,5 +13,5 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/actix_example/entity/src/lib.rs b/examples/actix_example/entity/src/lib.rs index e8b6291ac3..b85eeb2ad5 100644 --- a/examples/actix_example/entity/src/lib.rs +++ b/examples/actix_example/entity/src/lib.rs @@ -1 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub mod prelude; + pub mod post; diff --git a/examples/actix_example/entity/src/post.rs b/examples/actix_example/entity/src/post.rs index 62cde830cf..2e8ff73bbd 100644 --- a/examples/actix_example/entity/src/post.rs +++ b/examples/actix_example/entity/src/post.rs @@ -1,8 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] -#[sea_orm(table_name = "posts")] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] @@ -12,7 +15,4 @@ pub struct Model { pub text: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/actix_example/entity/src/prelude.rs b/examples/actix_example/entity/src/prelude.rs new file mode 100644 index 0000000000..0840a758e9 --- /dev/null +++ b/examples/actix_example/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub use super::post::Entity as Post; diff --git a/examples/actix_example/migration/Cargo.toml b/examples/actix_example/migration/Cargo.toml index 61187c6468..464b7fbf05 100644 --- a/examples/actix_example/migration/Cargo.toml +++ b/examples/actix_example/migration/Cargo.toml @@ -1,21 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-actix-native-tls", - # "sqlx-mysql", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-mysql", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/actix_example/migration/src/lib.rs b/examples/actix_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/actix_example/migration/src/lib.rs +++ b/examples/actix_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs b/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/actix_example/migration/src/m20220120_000002_seed_posts.rs b/examples/actix_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/actix_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/actix_example/migration/src/main.rs b/examples/actix_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/actix_example/migration/src/main.rs +++ b/examples/actix_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/axum_example/Cargo.toml b/examples/axum_example/Cargo.toml index 0387b657bb..e7781d9abb 100644 --- a/examples/axum_example/Cargo.toml +++ b/examples/axum_example/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "sea-orm-axum-example" -version = "0.1.0" -authors = ["Yoshiera Huang "] -edition = "2021" -publish = false +authors = ["Yoshiera Huang "] +edition = "2024" +name = "sea-orm-axum-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] axum-example-api = { path = "api" } diff --git a/examples/axum_example/README.md b/examples/axum_example/README.md index fe120e7151..6c07fe763c 100644 --- a/examples/axum_example/README.md +++ b/examples/axum_example/README.md @@ -4,15 +4,27 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-postgres",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-postgres",` line) 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock +cd api +cargo test ``` + +Run migration: + +```bash +cargo run -p migration -- up +``` + +Regenerate entity: + +```bash +sea-orm-cli generate entity --output-dir ./entity/src --lib --entity-format dense --with-serde both +``` \ No newline at end of file diff --git a/examples/axum_example/api/Cargo.toml b/examples/axum_example/api/Cargo.toml index 8b19a6da82..3a8c81c4e9 100644 --- a/examples/axum_example/api/Cargo.toml +++ b/examples/axum_example/api/Cargo.toml @@ -1,22 +1,39 @@ [package] -name = "axum-example-api" -version = "0.1.0" -authors = ["Yoshiera Huang "] -edition = "2021" -publish = false +authors = ["Yoshiera Huang "] +edition = "2024" +name = "axum-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -axum-example-core = { path = "../core" } -tokio = { version = "1.23.0", features = ["full"] } -axum = "0.6.1" -tower = "0.4.13" -tower-http = { version = "0.3.5", features = ["fs"] } -tower-cookies = "0.8.0" -anyhow = "1.0.66" -dotenvy = "0.15.6" -serde = "1.0.149" -serde_json = "1.0.89" -tera = "1.17.1" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -entity = { path = "../entity" } -migration = { path = "../migration" } +anyhow = "1.0.75" +axum = "0.8" +dotenvy = "0.15.7" +entity = { path = "../entity" } +migration = { path = "../migration" } +serde = "1.0.193" +serde_json = "1.0.108" +tera = "1.19.1" +tokio = { version = "1.34.0", features = ["full"] } +tower = "0.5" +tower-cookies = "0.11" +tower-http = { version = "0.6", features = ["fs"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version + +[dev-dependencies] +axum-example-api = { path = ".", features = ["sqlite"] } + +[features] +sqlite = ["sea-orm/sqlx-sqlite"] diff --git a/examples/axum_example/api/src/flash.rs b/examples/axum_example/api/src/flash.rs index 8f99a27af6..fcb7889e2e 100644 --- a/examples/axum_example/api/src/flash.rs +++ b/examples/axum_example/api/src/flash.rs @@ -1,5 +1,5 @@ -use axum::http::{header, HeaderMap, HeaderValue, StatusCode}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use axum::http::{HeaderMap, HeaderValue, StatusCode, header}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use tower_cookies::{Cookie, Cookies}; #[derive(Deserialize)] diff --git a/examples/axum_example/api/src/lib.rs b/examples/axum_example/api/src/lib.rs index a77a14b749..520e9d5dad 100644 --- a/examples/axum_example/api/src/lib.rs +++ b/examples/axum_example/api/src/lib.rs @@ -1,29 +1,29 @@ mod flash; +pub mod service; use axum::{ + Router, extract::{Form, Path, Query, State}, http::StatusCode, response::Html, routing::{get, get_service, post}, - Router, Server, -}; -use axum_example_core::{ - sea_orm::{Database, DatabaseConnection}, - Mutation as MutationCore, Query as QueryCore, }; use entity::post; -use flash::{get_flash_cookie, post_response, PostResponse}; +use flash::{PostResponse, get_flash_cookie, post_response}; use migration::{Migrator, MigratorTrait}; +use sea_orm::{Database, DatabaseConnection}; use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use std::{env, net::SocketAddr}; +use service::{Mutation, Query as QueryService}; +use std::env; use tera::Tera; use tower_cookies::{CookieManagerLayer, Cookies}; use tower_http::services::ServeDir; #[tokio::main] async fn start() -> anyhow::Result<()> { - env::set_var("RUST_LOG", "debug"); + unsafe { + env::set_var("RUST_LOG", "debug"); + } tracing_subscriber::fmt::init(); dotenvy::dotenv().ok(); @@ -44,16 +44,16 @@ async fn start() -> anyhow::Result<()> { let app = Router::new() .route("/", get(list_posts).post(create_post)) - .route("/:id", get(edit_post).post(update_post)) + .route("/{id}", get(edit_post).post(update_post)) .route("/new", get(new_post)) - .route("/delete/:id", post(delete_post)) + .route("/delete/{id}", post(delete_post)) .nest_service( "/static", get_service(ServeDir::new(concat!( env!("CARGO_MANIFEST_DIR"), "/static" ))) - .handle_error(|error: std::io::Error| async move { + .handle_error(|error| async move { ( StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {error}"), @@ -63,8 +63,8 @@ async fn start() -> anyhow::Result<()> { .layer(CookieManagerLayer::new()) .with_state(state); - let addr = SocketAddr::from_str(&server_url).unwrap(); - Server::bind(&addr).serve(app.into_make_service()).await?; + let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap(); + axum::serve(listener, app).await?; Ok(()) } @@ -95,7 +95,7 @@ async fn list_posts( let page = params.page.unwrap_or(1); let posts_per_page = params.posts_per_page.unwrap_or(5); - let (posts, num_pages) = QueryCore::find_posts_in_page(&state.conn, page, posts_per_page) + let (posts, num_pages) = QueryService::find_posts_in_page(&state.conn, page, posts_per_page) .await .expect("Cannot find posts in page"); @@ -134,13 +134,13 @@ async fn create_post( ) -> Result { let form = form.0; - MutationCore::create_post(&state.conn, form) + Mutation::create_post(&state.conn, form) .await .expect("could not insert post"); let data = FlashData { kind: "success".to_owned(), - message: "Post succcessfully added".to_owned(), + message: "Post successfully added".to_owned(), }; Ok(post_response(&mut cookies, data)) @@ -150,7 +150,7 @@ async fn edit_post( state: State, Path(id): Path, ) -> Result, (StatusCode, &'static str)> { - let post: post::Model = QueryCore::find_post_by_id(&state.conn, id) + let post: post::Model = QueryService::find_post_by_id(&state.conn, id) .await .expect("could not find post") .unwrap_or_else(|| panic!("could not find post with id {id}")); @@ -174,13 +174,13 @@ async fn update_post( ) -> Result { let form = form.0; - MutationCore::update_post_by_id(&state.conn, id, form) + Mutation::update_post_by_id(&state.conn, id, form) .await .expect("could not edit post"); let data = FlashData { kind: "success".to_owned(), - message: "Post succcessfully updated".to_owned(), + message: "Post successfully updated".to_owned(), }; Ok(post_response(&mut cookies, data)) @@ -191,13 +191,13 @@ async fn delete_post( Path(id): Path, mut cookies: Cookies, ) -> Result { - MutationCore::delete_post(&state.conn, id) + Mutation::delete_post(&state.conn, id) .await .expect("could not delete post"); let data = FlashData { kind: "success".to_owned(), - message: "Post succcessfully deleted".to_owned(), + message: "Post successfully deleted".to_owned(), }; Ok(post_response(&mut cookies, data)) diff --git a/examples/actix_example/core/src/lib.rs b/examples/axum_example/api/src/service/mod.rs similarity index 78% rename from examples/actix_example/core/src/lib.rs rename to examples/axum_example/api/src/service/mod.rs index 4a80f23913..4d0b4b4b4e 100644 --- a/examples/actix_example/core/src/lib.rs +++ b/examples/axum_example/api/src/service/mod.rs @@ -3,5 +3,3 @@ mod query; pub use mutation::*; pub use query::*; - -pub use sea_orm; diff --git a/examples/actix_example/core/src/mutation.rs b/examples/axum_example/api/src/service/mutation.rs similarity index 100% rename from examples/actix_example/core/src/mutation.rs rename to examples/axum_example/api/src/service/mutation.rs diff --git a/examples/actix_example/core/src/query.rs b/examples/axum_example/api/src/service/query.rs similarity index 100% rename from examples/actix_example/core/src/query.rs rename to examples/axum_example/api/src/service/query.rs diff --git a/examples/axum_example/api/tests/crud_tests.rs b/examples/axum_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..73cb646d38 --- /dev/null +++ b/examples/axum_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use axum_example_api::service::{Mutation, Query}; +use entity::post; +use sea_orm::Database; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/axum_example/core/Cargo.toml b/examples/axum_example/core/Cargo.toml deleted file mode 100644 index 7e78682a2a..0000000000 --- a/examples/axum_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "axum-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-postgres", - # "sqlx-mysql", - # "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/axum_example/core/tests/mock.rs b/examples/axum_example/core/tests/mock.rs deleted file mode 100644 index 8321053026..0000000000 --- a/examples/axum_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use axum_example_core::{Mutation, Query}; -use entity::post; -use prepare::prepare_mock_db; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/axum_example/core/tests/prepare.rs b/examples/axum_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/axum_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/axum_example/entity/Cargo.toml b/examples/axum_example/entity/Cargo.toml index 7dea367337..9e429a350b 100644 --- a/examples/axum_example/entity/Cargo.toml +++ b/examples/axum_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,5 +13,5 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/axum_example/entity/src/lib.rs b/examples/axum_example/entity/src/lib.rs index e8b6291ac3..b85eeb2ad5 100644 --- a/examples/axum_example/entity/src/lib.rs +++ b/examples/axum_example/entity/src/lib.rs @@ -1 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub mod prelude; + pub mod post; diff --git a/examples/axum_example/entity/src/post.rs b/examples/axum_example/entity/src/post.rs index 66a7b652f7..2e8ff73bbd 100644 --- a/examples/axum_example/entity/src/post.rs +++ b/examples/axum_example/entity/src/post.rs @@ -1,10 +1,11 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.3.2 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "posts")] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] @@ -14,7 +15,4 @@ pub struct Model { pub text: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/axum_example/entity/src/prelude.rs b/examples/axum_example/entity/src/prelude.rs new file mode 100644 index 0000000000..0840a758e9 --- /dev/null +++ b/examples/axum_example/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub use super::post::Entity as Post; diff --git a/examples/axum_example/migration/Cargo.toml b/examples/axum_example/migration/Cargo.toml index a59e5d0a04..f5c9543ea2 100644 --- a/examples/axum_example/migration/Cargo.toml +++ b/examples/axum_example/migration/Cargo.toml @@ -1,21 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-postgres", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-postgres", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/axum_example/migration/src/lib.rs b/examples/axum_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/axum_example/migration/src/lib.rs +++ b/examples/axum_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs b/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/axum_example/migration/src/m20220120_000002_seed_posts.rs b/examples/axum_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/axum_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/axum_example/migration/src/main.rs b/examples/axum_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/axum_example/migration/src/main.rs +++ b/examples/axum_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 4c36d6cc9f..8bd8f1a779 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -2,15 +2,17 @@ # A separate workspace [package] -name = "sea-orm-example-async-std" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-example-basic" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -async-std = { version = "1.9", features = [ "attributes", "tokio1" ] } -sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-async-std-native-tls" ] } -serde_json = { version = "1" } -futures = { version = "0.3" } -async-stream = { version = "0.3" } futures-util = { version = "0.3" } +sea-orm = { path = "../../", features = [ + "sqlx-mysql", + "runtime-tokio-native-tls", +] } +serde_json = { version = "1" } +tokio = { version = "1", features = ["full"] } diff --git a/examples/basic/Readme.md b/examples/basic/Readme.md index b355e250ab..0e774ca50b 100644 --- a/examples/basic/Readme.md +++ b/examples/basic/Readme.md @@ -1,4 +1,4 @@ -# SeaORM SQLx example +# SeaORM MySQL example Prepare: @@ -18,7 +18,7 @@ find all fruits: SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fr Model { id: 1, name: "Blueberry", cake_id: Some(1) } -Model { id: 2, name: "Rasberry", cake_id: Some(1) } +Model { id: 2, name: "Raspberry", cake_id: Some(1) } Model { id: 3, name: "Strawberry", cake_id: Some(2) } @@ -48,7 +48,7 @@ find models belong to: SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FR Model { id: 1, name: "Blueberry", cake_id: Some(1) } -Model { id: 2, name: "Rasberry", cake_id: Some(1) } +Model { id: 2, name: "Raspberry", cake_id: Some(1) } ===== ===== @@ -58,7 +58,7 @@ SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` IN (1, 1, 2, NULL, NULL, NULL, NULL, NULL, NULL) (Model { id: 1, name: "Blueberry", cake_id: Some(1) }, Some(Model { id: 1, name: "New York Cheese" })) -(Model { id: 2, name: "Rasberry", cake_id: Some(1) }, Some(Model { id: 1, name: "New York Cheese" })) +(Model { id: 2, name: "Raspberry", cake_id: Some(1) }, Some(Model { id: 1, name: "New York Cheese" })) (Model { id: 3, name: "Strawberry", cake_id: Some(2) }, Some(Model { id: 2, name: "Chocolate Forest" })) (Model { id: 4, name: "Apple", cake_id: None }, None) (Model { id: 5, name: "Banana", cake_id: None }, None) @@ -73,7 +73,7 @@ with loader: SELECT `cake`.`id`, `cake`.`name` FROM `cake` SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` WHERE `fruit`.`cake_id` IN (1, 2) -(Model { id: 1, name: "New York Cheese" }, [Model { id: 1, name: "Blueberry", cake_id: Some(1) }, Model { id: 2, name: "Rasberry", cake_id: Some(1) }]) +(Model { id: 1, name: "New York Cheese" }, [Model { id: 1, name: "Blueberry", cake_id: Some(1) }, Model { id: 2, name: "Raspberry", cake_id: Some(1) }]) (Model { id: 2, name: "Chocolate Forest" }, [Model { id: 3, name: "Strawberry", cake_id: Some(2) }]) @@ -122,7 +122,7 @@ SELECT `cake`.`id`, `cake`.`name` FROM `cake` LIMIT 3 OFFSET 3 find all fruits paginated: SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 0 Model { id: 1, name: "Blueberry", cake_id: Some(1) } -Model { id: 2, name: "Rasberry", cake_id: Some(1) } +Model { id: 2, name: "Raspberry", cake_id: Some(1) } Model { id: 3, name: "Strawberry", cake_id: Some(2) } SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 3 Model { id: 4, name: "Apple", cake_id: None } @@ -137,7 +137,7 @@ SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFS find all fruits with stream: SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 0 Model { id: 1, name: "Blueberry", cake_id: Some(1) } -Model { id: 2, name: "Rasberry", cake_id: Some(1) } +Model { id: 2, name: "Raspberry", cake_id: Some(1) } Model { id: 3, name: "Strawberry", cake_id: Some(2) } SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 3 Model { id: 4, name: "Apple", cake_id: None } @@ -152,7 +152,7 @@ SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFS find all fruits in json with stream: SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 0 Object {"cake_id": Number(1), "id": Number(1), "name": String("Blueberry")} -Object {"cake_id": Number(1), "id": Number(2), "name": String("Rasberry")} +Object {"cake_id": Number(1), "id": Number(2), "name": String("Raspberry")} Object {"cake_id": Number(2), "id": Number(3), "name": String("Strawberry")} SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 3 Object {"cake_id": Null, "id": Number(4), "name": String("Apple")} @@ -168,7 +168,7 @@ SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFS fruits first page: SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit` LIMIT 3 OFFSET 0 Model { id: 1, name: "Blueberry", cake_id: Some(1) } -Model { id: 2, name: "Rasberry", cake_id: Some(1) } +Model { id: 2, name: "Raspberry", cake_id: Some(1) } Model { id: 3, name: "Strawberry", cake_id: Some(2) } ===== ===== diff --git a/examples/basic/bakery.sql b/examples/basic/bakery.sql index 763328dc82..a7dd20581a 100644 --- a/examples/basic/bakery.sql +++ b/examples/basic/bakery.sql @@ -22,7 +22,7 @@ CREATE TABLE `fruit` ( INSERT INTO `fruit` (`id`, `name`, `cake_id`) VALUES (1, 'Blueberry', 1), - (2, 'Rasberry', 1), + (2, 'Raspberry', 1), (3, 'Strawberry', 2); INSERT INTO `fruit` (`name`, `cake_id`) VALUES diff --git a/examples/basic/import.sh b/examples/basic/import.sh deleted file mode 100644 index 7e4820c1af..0000000000 --- a/examples/basic/import.sh +++ /dev/null @@ -1,7 +0,0 @@ -cp ../../src/tests_cfg/cake.rs src/example_cake.rs -cp ../../src/tests_cfg/fruit.rs src/example_fruit.rs -cp ../../src/tests_cfg/filling.rs src/example_filling.rs -cp ../../src/tests_cfg/cake_filling.rs src/example_cake_filling.rs - -sed -i 's/^use crate::/use sea_orm::/g' src/*.rs -sed -i '/^use crate as sea_orm;/d' src/*.rs \ No newline at end of file diff --git a/examples/basic/src/entity/cake.rs b/examples/basic/src/entity/cake.rs new file mode 100644 index 0000000000..d352cd13c8 --- /dev/null +++ b/examples/basic/src/entity/cake.rs @@ -0,0 +1,19 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", nullable)] + pub name: Option, + #[sea_orm(has_many)] + pub fruits: HasMany, + #[sea_orm(has_many, via = "cake_filling")] + pub fillings: HasMany, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/basic/src/entity/cake_filling.rs b/examples/basic/src/entity/cake_filling.rs new file mode 100644 index 0000000000..a99c53f3c1 --- /dev/null +++ b/examples/basic/src/entity/cake_filling.rs @@ -0,0 +1,31 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_filling")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub filling_id: i32, + #[sea_orm( + belongs_to, + from = "cake_id", + to = "id", + on_update = "Cascade", + on_delete = "Cascade" + )] + pub cake: HasOne, + #[sea_orm( + belongs_to, + from = "filling_id", + to = "id", + on_update = "Cascade", + on_delete = "Cascade" + )] + pub filling: HasOne, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/basic/src/entity/filling.rs b/examples/basic/src/entity/filling.rs new file mode 100644 index 0000000000..ebd7bc801e --- /dev/null +++ b/examples/basic/src/entity/filling.rs @@ -0,0 +1,16 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "filling")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(has_many, via = "cake_filling")] + pub cakes: HasMany, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/basic/src/entity/fruit.rs b/examples/basic/src/entity/fruit.rs new file mode 100644 index 0000000000..21cfc120ef --- /dev/null +++ b/examples/basic/src/entity/fruit.rs @@ -0,0 +1,17 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "fruit")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub cake_id: Option, + #[sea_orm(belongs_to, from = "cake_id", to = "id")] + pub cake: HasOne, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/basic/src/entity/mod.rs b/examples/basic/src/entity/mod.rs new file mode 100644 index 0000000000..c45704e5e2 --- /dev/null +++ b/examples/basic/src/entity/mod.rs @@ -0,0 +1,11 @@ +pub mod cake; +pub mod cake_filling; +pub mod filling; +pub mod fruit; + +pub mod sea_orm_active_enums; + +pub use cake::Entity as Cake; +pub use cake_filling::Entity as CakeFilling; +pub use filling::Entity as Filling; +pub use fruit::Entity as Fruit; diff --git a/examples/basic/src/sea_orm_active_enums.rs b/examples/basic/src/entity/sea_orm_active_enums.rs similarity index 100% rename from examples/basic/src/sea_orm_active_enums.rs rename to examples/basic/src/entity/sea_orm_active_enums.rs diff --git a/examples/basic/src/example_cake.rs b/examples/basic/src/example_cake.rs deleted file mode 100644 index 538443c966..0000000000 --- a/examples/basic/src/example_cake.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! The `cake` entity. - -use sea_orm::entity::prelude::*; - -/// Cake entity -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "cake")] -pub struct Model { - #[sea_orm(primary_key)] - /// id field - pub id: i32, - /// name field - pub name: String, -} - -/// Cake relation -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - /// Fruit relation - Fruit, -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::Fruit => Entity::has_many(super::fruit::Entity).into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Fruit.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::cake_filling::Relation::Filling.def() - } - - fn via() -> Option { - Some(super::cake_filling::Relation::Cake.def().rev()) - } -} - -#[async_trait::async_trait] -impl ActiveModelBehavior for ActiveModel { - async fn before_save(self, _db: &C, _insert: bool) -> Result - where - C: ConnectionTrait, - { - Ok(self) - } -} diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 58c09db007..8c69a91649 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -4,36 +4,30 @@ use sea_orm::Database; -mod entities; -pub mod example_cake; -pub mod example_cake_filling; -pub mod example_filling; -pub mod example_fruit; -mod operation; -pub mod sea_orm_active_enums; -mod select; - -use entities::*; -use example_cake as cake; -use example_cake_filling as cake_filling; -use example_filling as filling; -use example_fruit as fruit; -use operation::*; -use select::*; - -#[async_std::main] +mod entity; +mod mutation; +mod query; + +use entity::*; +use mutation::*; +use query::*; + +#[tokio::main] async fn main() { - let db = Database::connect("sql://sea:sea@localhost/bakery") - .await - .unwrap(); + let db = Database::connect( + std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "mysql://sea:sea@localhost/bakery".to_owned()), + ) + .await + .unwrap(); println!("{db:?}\n"); println!("===== =====\n"); - all_about_select(&db).await.unwrap(); + all_about_query(&db).await.unwrap(); println!("===== =====\n"); - all_about_operation(&db).await.unwrap(); + all_about_mutation(&db).await.unwrap(); } diff --git a/examples/basic/src/operation.rs b/examples/basic/src/mutation.rs similarity index 94% rename from examples/basic/src/operation.rs rename to examples/basic/src/mutation.rs index ceae382a75..65f0389941 100644 --- a/examples/basic/src/operation.rs +++ b/examples/basic/src/mutation.rs @@ -1,7 +1,7 @@ use super::*; -use sea_orm::{entity::*, error::*, DbConn}; +use sea_orm::{DbConn, entity::*, error::*}; -pub async fn all_about_operation(db: &DbConn) -> Result<(), DbErr> { +pub async fn all_about_mutation(db: &DbConn) -> Result<(), DbErr> { insert_and_update(db).await?; println!("===== =====\n"); diff --git a/examples/basic/src/select.rs b/examples/basic/src/query.rs similarity index 97% rename from examples/basic/src/select.rs rename to examples/basic/src/query.rs index bb1354fba6..d2ad3cc393 100644 --- a/examples/basic/src/select.rs +++ b/examples/basic/src/query.rs @@ -1,7 +1,7 @@ use super::*; -use sea_orm::{entity::*, error::*, query::*, DbConn, FromQueryResult}; +use sea_orm::{DbConn, FromQueryResult, entity::*, error::*, query::*}; -pub async fn all_about_select(db: &DbConn) -> Result<(), DbErr> { +pub async fn all_about_query(db: &DbConn) -> Result<(), DbErr> { find_all(db).await?; println!("===== =====\n"); @@ -258,7 +258,7 @@ async fn find_together_json(db: &DbConn) -> Result<(), DbErr> { print!("find cakes and fruits: "); let cakes_fruits = Cake::find() - .find_with_related(Fruit) + .find_also_related(Fruit) .into_json() .all(db) .await?; @@ -290,9 +290,9 @@ async fn count_fruits_by_cake_json(db: &DbConn) -> Result<(), DbErr> { } async fn find_all_stream(db: &DbConn) -> Result<(), DbErr> { - use async_std::task::sleep; - use futures::TryStreamExt; + use futures_util::TryStreamExt; use std::time::Duration; + use tokio::time::sleep; println!("find all cakes paginated: "); let mut cake_paginator = cake::Entity::find().paginate(db, 3); diff --git a/examples/graphql_example/.env b/examples/graphql_example/.env index f1cc324cd6..f1ee0b46a6 100644 --- a/examples/graphql_example/.env +++ b/examples/graphql_example/.env @@ -1 +1 @@ -DATABASE_URL=sqlite:./db?mode=rwc \ No newline at end of file +DATABASE_URL="sqlite://notes.db?mode=rwc" \ No newline at end of file diff --git a/examples/graphql_example/Cargo.toml b/examples/graphql_example/Cargo.toml index c3e342dc3b..b2a1bd2fed 100644 --- a/examples/graphql_example/Cargo.toml +++ b/examples/graphql_example/Cargo.toml @@ -1,13 +1,14 @@ [package] -name = "sea-orm-graphql-example" -authors = ["Aaron Leopold "] -version = "0.1.0" -edition = "2021" -publish = false +authors = ["Aaron Leopold "] +edition = "2024" +name = "sea-orm-graphql-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] graphql-example-api = { path = "api" } diff --git a/examples/graphql_example/README.md b/examples/graphql_example/README.md index 351c7df557..45988d0dd7 100644 --- a/examples/graphql_example/README.md +++ b/examples/graphql_example/README.md @@ -6,15 +6,21 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server 1. Visit [localhost:3000/api/graphql](http://localhost:3000/api/graphql) in browser -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock +cd api +cargo test +``` + +Run migration: + +```bash +cargo run -p migration -- up ``` diff --git a/examples/graphql_example/api/Cargo.toml b/examples/graphql_example/api/Cargo.toml index aecf0466ec..5b1a6aaad2 100644 --- a/examples/graphql_example/api/Cargo.toml +++ b/examples/graphql_example/api/Cargo.toml @@ -1,15 +1,27 @@ [package] -name = "graphql-example-api" -authors = ["Aaron Leopold "] -version = "0.1.0" -edition = "2021" -publish = false +authors = ["Aaron Leopold "] +edition = "2024" +name = "graphql-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -graphql-example-core = { path = "../core" } -tokio = { version = "1.0", features = ["full"] } -axum = "0.5.1" -dotenvy = "0.15.0" -async-graphql-axum = "4.0.6" -entity = { path = "../entity" } -migration = { path = "../migration" } +async-graphql = { version = "7.0.17" } +async-graphql-axum = "7.0" +axum = "0.8" +dotenvy = "0.15.7" +entity = { path = "../entity" } +migration = { path = "../migration" } +tokio = { version = "1.29", features = ["full"] } + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + # "sqlx-postgres", + # "sqlx-mysql", + "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/graphql_example/api/src/db.rs b/examples/graphql_example/api/src/db.rs index a1b79cd414..3cc41e27b3 100644 --- a/examples/graphql_example/api/src/db.rs +++ b/examples/graphql_example/api/src/db.rs @@ -1,4 +1,4 @@ -use graphql_example_core::sea_orm::DatabaseConnection; +use sea_orm::DatabaseConnection; pub struct Database { pub connection: DatabaseConnection, @@ -6,11 +6,9 @@ pub struct Database { impl Database { pub async fn new() -> Self { - let connection = graphql_example_core::sea_orm::Database::connect( - std::env::var("DATABASE_URL").unwrap(), - ) - .await - .expect("Could not connect to database"); + let connection = sea_orm::Database::connect(std::env::var("DATABASE_URL").unwrap()) + .await + .expect("Could not connect to database"); Database { connection } } diff --git a/examples/graphql_example/api/src/graphql/mutation/mod.rs b/examples/graphql_example/api/src/graphql/mutation/mod.rs index fb3c483abb..7575353d67 100644 --- a/examples/graphql_example/api/src/graphql/mutation/mod.rs +++ b/examples/graphql_example/api/src/graphql/mutation/mod.rs @@ -1,5 +1,3 @@ -use entity::async_graphql; - pub mod note; pub use note::NoteMutation; diff --git a/examples/graphql_example/api/src/graphql/mutation/note.rs b/examples/graphql_example/api/src/graphql/mutation/note.rs index a3ce9f48f6..b5d975335e 100644 --- a/examples/graphql_example/api/src/graphql/mutation/note.rs +++ b/examples/graphql_example/api/src/graphql/mutation/note.rs @@ -1,7 +1,6 @@ -use async_graphql::{Context, Object, Result}; -use entity::async_graphql::{self, InputObject, SimpleObject}; +use crate::service::Mutation; +use async_graphql::{Context, InputObject, Object, Result, SimpleObject}; use entity::note; -use graphql_example_core::Mutation; use crate::db::Database; diff --git a/examples/graphql_example/api/src/graphql/query/mod.rs b/examples/graphql_example/api/src/graphql/query/mod.rs index b193647ab1..04e606e98f 100644 --- a/examples/graphql_example/api/src/graphql/query/mod.rs +++ b/examples/graphql_example/api/src/graphql/query/mod.rs @@ -1,5 +1,3 @@ -use entity::async_graphql; - pub mod note; pub use note::NoteQuery; diff --git a/examples/graphql_example/api/src/graphql/query/note.rs b/examples/graphql_example/api/src/graphql/query/note.rs index 1ac2549d9e..3621504310 100644 --- a/examples/graphql_example/api/src/graphql/query/note.rs +++ b/examples/graphql_example/api/src/graphql/query/note.rs @@ -1,6 +1,6 @@ +use crate::service::Query; use async_graphql::{Context, Object, Result}; -use entity::{async_graphql, note}; -use graphql_example_core::Query; +use entity::note; use crate::db::Database; diff --git a/examples/graphql_example/api/src/graphql/schema.rs b/examples/graphql_example/api/src/graphql/schema.rs index 6224c409e9..03bda59649 100644 --- a/examples/graphql_example/api/src/graphql/schema.rs +++ b/examples/graphql_example/api/src/graphql/schema.rs @@ -1,5 +1,5 @@ +use async_graphql; use async_graphql::{EmptySubscription, Schema}; -use entity::async_graphql; use migration::{Migrator, MigratorTrait}; use crate::{ diff --git a/examples/graphql_example/api/src/lib.rs b/examples/graphql_example/api/src/lib.rs index 85275fcbe1..31130518b0 100644 --- a/examples/graphql_example/api/src/lib.rs +++ b/examples/graphql_example/api/src/lib.rs @@ -1,22 +1,21 @@ mod db; mod graphql; +pub mod service; -use entity::async_graphql; - -use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; +use async_graphql::http::{GraphQLPlaygroundConfig, playground_source}; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::{ - extract::Extension, + Router, + extract::State, response::{Html, IntoResponse}, routing::get, - Router, }; -use graphql::schema::{build_schema, AppSchema}; +use graphql::schema::{AppSchema, build_schema}; #[cfg(debug_assertions)] use dotenvy::dotenv; -async fn graphql_handler(schema: Extension, req: GraphQLRequest) -> GraphQLResponse { +async fn graphql_handler(schema: State, req: GraphQLRequest) -> GraphQLResponse { schema.execute(req.into_inner()).await.into() } @@ -38,12 +37,10 @@ pub async fn main() { "/api/graphql", get(graphql_playground).post(graphql_handler), ) - .layer(Extension(schema)); + .with_state(schema); println!("Playground: http://localhost:3000/api/graphql"); - axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); } diff --git a/examples/axum_example/core/src/lib.rs b/examples/graphql_example/api/src/service/mod.rs similarity index 78% rename from examples/axum_example/core/src/lib.rs rename to examples/graphql_example/api/src/service/mod.rs index 4a80f23913..4d0b4b4b4e 100644 --- a/examples/axum_example/core/src/lib.rs +++ b/examples/graphql_example/api/src/service/mod.rs @@ -3,5 +3,3 @@ mod query; pub use mutation::*; pub use query::*; - -pub use sea_orm; diff --git a/examples/graphql_example/core/src/mutation.rs b/examples/graphql_example/api/src/service/mutation.rs similarity index 93% rename from examples/graphql_example/core/src/mutation.rs rename to examples/graphql_example/api/src/service/mutation.rs index 1f2447fba2..f6c331439e 100644 --- a/examples/graphql_example/core/src/mutation.rs +++ b/examples/graphql_example/api/src/service/mutation.rs @@ -1,5 +1,5 @@ -use ::entity::{note, note::Entity as Note}; -use sea_orm::*; +use entity::{note, note::Entity as Note}; +use sea_orm::{DbConn, DbErr, DeleteResult, entity::*}; pub struct Mutation; diff --git a/examples/graphql_example/core/src/query.rs b/examples/graphql_example/api/src/service/query.rs similarity index 87% rename from examples/graphql_example/core/src/query.rs rename to examples/graphql_example/api/src/service/query.rs index 7b6dc1b7d5..eac66de1ba 100644 --- a/examples/graphql_example/core/src/query.rs +++ b/examples/graphql_example/api/src/service/query.rs @@ -1,5 +1,5 @@ -use ::entity::{note, note::Entity as Note}; -use sea_orm::*; +use entity::{note, note::Entity as Note}; +use sea_orm::{DbConn, DbErr, EntityTrait, PaginatorTrait, QueryOrder}; pub struct Query; diff --git a/examples/graphql_example/api/tests/crud_tests.rs b/examples/graphql_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..cd31208639 --- /dev/null +++ b/examples/graphql_example/api/tests/crud_tests.rs @@ -0,0 +1,103 @@ +use entity::note; +use graphql_example_api::service::{Mutation, Query}; +use sea_orm::{ConnectionTrait, Database, Schema}; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.execute(&Schema::new(db.get_database_backend()).create_table_from_entity(note::Entity)) + .await + .unwrap(); + + { + let note = Mutation::create_note( + db, + note::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + note, + note::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + } + ); + } + + { + let note = Mutation::create_note( + db, + note::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + note, + note::Model { + id: 2, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + } + ); + } + + { + let note = Query::find_note_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(note.id, 1); + assert_eq!(note.title, "Title A"); + } + + { + let note = Mutation::update_note_by_id( + db, + 1, + note::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + note, + note::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_note(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let note = Query::find_note_by_id(db, 2).await.unwrap(); + assert!(note.is_none()); + } + + { + let result = Mutation::delete_all_notes(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/graphql_example/core/Cargo.toml b/examples/graphql_example/core/Cargo.toml deleted file mode 100644 index c9984c74b6..0000000000 --- a/examples/graphql_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "graphql-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-async-std-native-tls", - # "sqlx-postgres", - # "sqlx-mysql", - "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/graphql_example/core/tests/mock.rs b/examples/graphql_example/core/tests/mock.rs deleted file mode 100644 index 16a56189f5..0000000000 --- a/examples/graphql_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::note; -use graphql_example_core::{Mutation, Query}; -use prepare::prepare_mock_db; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let note = Query::find_note_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(note.id, 1); - } - - { - let note = Query::find_note_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(note.id, 5); - } - - { - let note = Mutation::create_note( - db, - note::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - note, - note::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - } - ); - } - - { - let note = Mutation::update_note_by_id( - db, - 1, - note::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - note, - note::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_note(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_notes(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/graphql_example/core/tests/prepare.rs b/examples/graphql_example/core/tests/prepare.rs deleted file mode 100644 index 34541a4355..0000000000 --- a/examples/graphql_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::note; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [note::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [note::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [note::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [note::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [note::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [note::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/graphql_example/entity/Cargo.toml b/examples/graphql_example/entity/Cargo.toml index 21a849b904..91b5eb7183 100644 --- a/examples/graphql_example/entity/Cargo.toml +++ b/examples/graphql_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,8 +13,8 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.async-graphql] -version = "4.0.6" +version = "7.0" [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/graphql_example/entity/src/lib.rs b/examples/graphql_example/entity/src/lib.rs index d87caf39ce..f20ccb595d 100644 --- a/examples/graphql_example/entity/src/lib.rs +++ b/examples/graphql_example/entity/src/lib.rs @@ -1,3 +1 @@ pub mod note; - -pub use async_graphql; diff --git a/examples/graphql_example/entity/src/note.rs b/examples/graphql_example/entity/src/note.rs index a03f6d3f85..24bfc3c51e 100644 --- a/examples/graphql_example/entity/src/note.rs +++ b/examples/graphql_example/entity/src/note.rs @@ -1,9 +1,9 @@ use async_graphql::*; -use sea_orm::{entity::prelude::*, DeleteMany}; +use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)] -#[sea_orm(table_name = "notes")] +#[sea_orm(table_name = "note")] #[graphql(concrete(name = "Note", params()))] pub struct Model { #[sea_orm(primary_key)] @@ -19,15 +19,7 @@ pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} impl Entity { - pub fn find_by_id(id: i32) -> Select { - Self::find().filter(Column::Id.eq(id)) - } - pub fn find_by_title(title: &str) -> Select { Self::find().filter(Column::Title.eq(title)) } - - pub fn delete_by_id(id: i32) -> DeleteMany { - Self::delete_many().filter(Column::Id.eq(id)) - } } diff --git a/examples/graphql_example/migration/Cargo.toml b/examples/graphql_example/migration/Cargo.toml index 6661c56568..9b7dfb86c7 100644 --- a/examples/graphql_example/migration/Cargo.toml +++ b/examples/graphql_example/migration/Cargo.toml @@ -1,22 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -dotenvy = "0.15.0" -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-sqlite", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-sqlite", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/graphql_example/migration/src/lib.rs b/examples/graphql_example/migration/src/lib.rs index 2c605afb94..e244b2e8e2 100644 --- a/examples/graphql_example/migration/src/lib.rs +++ b/examples/graphql_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; -mod m20220101_000001_create_table; +mod m20220120_000001_create_note_table; +mod m20220120_000002_seed_notes; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220101_000001_create_table::Migration)] + vec![ + Box::new(m20220120_000001_create_note_table::Migration), + Box::new(m20220120_000002_seed_notes::Migration), + ] } } diff --git a/examples/graphql_example/migration/src/m20220101_000001_create_table.rs b/examples/graphql_example/migration/src/m20220101_000001_create_table.rs deleted file mode 100644 index b7ec5a4d6b..0000000000 --- a/examples/graphql_example/migration/src/m20220101_000001_create_table.rs +++ /dev/null @@ -1,42 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Notes::Table) - .if_not_exists() - .col( - ColumnDef::new(Notes::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Notes::Title).string().not_null()) - .col(ColumnDef::new(Notes::Text).string().not_null()) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Notes::Table).to_owned()) - .await - } -} - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Notes { - Table, - Id, - Title, - Text, -} diff --git a/examples/graphql_example/migration/src/m20220120_000001_create_note_table.rs b/examples/graphql_example/migration/src/m20220120_000001_create_note_table.rs new file mode 100644 index 0000000000..c1117454f1 --- /dev/null +++ b/examples/graphql_example/migration/src/m20220120_000001_create_note_table.rs @@ -0,0 +1,17 @@ +use entity::note; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.get_schema_builder() + .register(note::Entity) + .sync(db) + .await + } +} diff --git a/examples/graphql_example/migration/src/m20220120_000002_seed_notes.rs b/examples/graphql_example/migration/src/m20220120_000002_seed_notes.rs new file mode 100644 index 0000000000..40b623a373 --- /dev/null +++ b/examples/graphql_example/migration/src/m20220120_000002_seed_notes.rs @@ -0,0 +1,43 @@ +use entity::note; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Note", "This is the first note."), + ("Second Note", "This is another note."), + ]; + + for (title, text) in seed_data { + let model = note::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Notes table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Note", "Second Note"]; + note::Entity::delete_many() + .filter(note::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Notes seeded data removed."); + Ok(()) + } +} diff --git a/examples/graphql_example/migration/src/main.rs b/examples/graphql_example/migration/src/main.rs index 89075f818b..f054deaf88 100644 --- a/examples/graphql_example/migration/src/main.rs +++ b/examples/graphql_example/migration/src/main.rs @@ -1,24 +1,6 @@ use sea_orm_migration::prelude::*; -#[cfg(debug_assertions)] -use dotenvy::dotenv; - -#[async_std::main] +#[tokio::main] async fn main() { - #[cfg(debug_assertions)] - dotenv().ok(); - - let fallback = "sqlite:./db?mode=rwc"; - - match std::env::var("DATABASE_URL") { - Ok(val) => { - println!("Using DATABASE_URL: {val}"); - } - Err(_) => { - std::env::set_var("DATABASE_URL", fallback); - println!("Set DATABASE_URL: {fallback}"); - } - }; - cli::run_cli(migration::Migrator).await; } diff --git a/examples/jsonrpsee_example/.env b/examples/jsonrpsee_example/.env index acd83c0e1f..e3e391d5a2 100644 --- a/examples/jsonrpsee_example/.env +++ b/examples/jsonrpsee_example/.env @@ -1,4 +1,3 @@ HOST=127.0.0.1 PORT=8000 -#DATABASE_URL="mysql://root:@localhost/poem_example" -DATABASE_URL="sqlite::memory:" +DATABASE_URL="sqlite://posts.db?mode=rwc" diff --git a/examples/jsonrpsee_example/Cargo.toml b/examples/jsonrpsee_example/Cargo.toml index 7365aa551c..4c7e717ad4 100644 --- a/examples/jsonrpsee_example/Cargo.toml +++ b/examples/jsonrpsee_example/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "sea-orm-jsonrpsee-example" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-jsonrpsee-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] jsonrpsee-example-api = { path = "api" } diff --git a/examples/jsonrpsee_example/README.md b/examples/jsonrpsee_example/README.md index 4d45e5b640..07cd0286de 100644 --- a/examples/jsonrpsee_example/README.md +++ b/examples/jsonrpsee_example/README.md @@ -2,14 +2,15 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server 1. Send jsonrpc request to server +#### Insert + ```shell -#insert curl --location --request POST 'http://127.0.0.1:8000' \ --header 'Content-Type: application/json' \ --data-raw '{"jsonrpc": "2.0", "method": "Post.Insert", "params": [ @@ -19,8 +20,11 @@ curl --location --request POST 'http://127.0.0.1:8000' \ "text":"aaaaaaa" } ], "id": 2}' +``` -#list +#### List + +```shell curl --location --request POST 'http://127.0.0.1:8000' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -32,8 +36,11 @@ curl --location --request POST 'http://127.0.0.1:8000' \ ], "id": 2 }' +``` + +#### Delete -#delete +```shell curl --location --request POST 'http://127.0.0.1:8000' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -44,8 +51,11 @@ curl --location --request POST 'http://127.0.0.1:8000' \ ], "id": 2 }' +``` -#update +#### Update + +```shell curl --location --request POST 'http://127.0.0.1:8000' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -60,12 +70,23 @@ curl --location --request POST 'http://127.0.0.1:8000' \ ], "id": 2 }' +``` + +Run tests: +```bash +cd api +cargo test ``` -Run mock test on the core logic crate: +Run migration: ```bash -cd core -cargo test --features mock +cargo run -p migration -- up ``` + +Regenerate entity: + +```bash +sea-orm-cli generate entity --output-dir ./entity/src --lib --entity-format dense --with-serde both +``` \ No newline at end of file diff --git a/examples/jsonrpsee_example/api/Cargo.toml b/examples/jsonrpsee_example/api/Cargo.toml index 6440e00417..fcce5a4725 100644 --- a/examples/jsonrpsee_example/api/Cargo.toml +++ b/examples/jsonrpsee_example/api/Cargo.toml @@ -1,19 +1,30 @@ [package] -name = "jsonrpsee-example-api" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "jsonrpsee-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -jsonrpsee-example-core = { path = "../core" } -jsonrpsee = { version = "0.8.0", features = ["full"] } -jsonrpsee-core = "0.9.0" -tokio = { version = "1.8.0", features = ["full"] } -serde = { version = "1", features = ["derive"] } -dotenvy = "0.15" -entity = { path = "../entity" } -migration = { path = "../migration" } -anyhow = "1.0.52" -async-trait = "0.1.52" -log = { version = "0.4", features = ["std"] } -simplelog = "0.12" +anyhow = "1.0.71" +async-trait = "0.1.71" +dotenvy = "0.15" +entity = { path = "../entity" } +jsonrpsee = { version = "0.18.2", features = ["full"] } +jsonrpsee-core = "0.18.2" +log = { version = "0.4", features = ["std"] } +migration = { path = "../migration" } +serde = { version = "1", features = ["derive"] } +simplelog = "0.12" +tokio = { version = "1.29.1", features = ["full"] } + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-sqlite", + # "sqlx-postgres", + # "sqlx-mysql", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/jsonrpsee_example/api/src/lib.rs b/examples/jsonrpsee_example/api/src/lib.rs index 5eace0d137..fae5854274 100644 --- a/examples/jsonrpsee_example/api/src/lib.rs +++ b/examples/jsonrpsee_example/api/src/lib.rs @@ -1,20 +1,21 @@ +pub mod service; + use std::env; -use anyhow::anyhow; use entity::post; -use jsonrpsee::core::{async_trait, RpcResult}; -use jsonrpsee::http_server::HttpServerBuilder; +use jsonrpsee::core::{RpcResult, async_trait}; use jsonrpsee::proc_macros::rpc; -use jsonrpsee::types::error::CallError; -use jsonrpsee_example_core::sea_orm::{Database, DatabaseConnection}; -use jsonrpsee_example_core::{Mutation, Query}; +use jsonrpsee::server::ServerBuilder; +use jsonrpsee::types::error::ErrorObjectOwned; use log::info; use migration::{Migrator, MigratorTrait}; +use sea_orm::{Database, DatabaseConnection}; +use service::{Mutation, Query}; use simplelog::*; use std::fmt::Display; use std::net::SocketAddr; use tokio::signal::ctrl_c; -use tokio::signal::unix::{signal, SignalKind}; +// use tokio::signal::unix::{signal, SignalKind}; const DEFAULT_POSTS_PER_PAGE: u64 = 5; @@ -88,7 +89,9 @@ where E: Display, { fn internal_call_error(self) -> RpcResult { - self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e)))) + // Err(ErrorObjectOwned::owned(1, "c", None::<()>)) + // self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e)))) + self.map_err(|e| ErrorObjectOwned::owned(1, format!("{}", e), None::<()>)) } } @@ -112,8 +115,9 @@ async fn start() -> std::io::Result<()> { let conn = Database::connect(&db_url).await.unwrap(); Migrator::up(&conn, None).await.unwrap(); - let server = HttpServerBuilder::default() + let server = ServerBuilder::default() .build(server_url.parse::().unwrap()) + .await .unwrap(); let rpc_impl = PpcImpl { conn }; @@ -121,12 +125,12 @@ async fn start() -> std::io::Result<()> { let handle = server.start(rpc_impl.into_rpc()).unwrap(); info!("starting listening {}", server_addr); - let mut sig_int = signal(SignalKind::interrupt()).unwrap(); - let mut sig_term = signal(SignalKind::terminate()).unwrap(); + // let mut sig_int = signal(SignalKind::interrupt()).unwrap(); + // let mut sig_term = signal(SignalKind::terminate()).unwrap(); tokio::select! { - _ = sig_int.recv() => info!("receive SIGINT"), - _ = sig_term.recv() => info!("receive SIGTERM"), + // _ = sig_int.recv() => info!("receive SIGINT"), + // _ = sig_term.recv() => info!("receive SIGTERM"), _ = ctrl_c() => info!("receive Ctrl C"), } handle.stop().unwrap(); diff --git a/examples/graphql_example/core/src/lib.rs b/examples/jsonrpsee_example/api/src/service/mod.rs similarity index 78% rename from examples/graphql_example/core/src/lib.rs rename to examples/jsonrpsee_example/api/src/service/mod.rs index 4a80f23913..4d0b4b4b4e 100644 --- a/examples/graphql_example/core/src/lib.rs +++ b/examples/jsonrpsee_example/api/src/service/mod.rs @@ -3,5 +3,3 @@ mod query; pub use mutation::*; pub use query::*; - -pub use sea_orm; diff --git a/examples/axum_example/core/src/mutation.rs b/examples/jsonrpsee_example/api/src/service/mutation.rs similarity index 100% rename from examples/axum_example/core/src/mutation.rs rename to examples/jsonrpsee_example/api/src/service/mutation.rs diff --git a/examples/axum_example/core/src/query.rs b/examples/jsonrpsee_example/api/src/service/query.rs similarity index 100% rename from examples/axum_example/core/src/query.rs rename to examples/jsonrpsee_example/api/src/service/query.rs diff --git a/examples/jsonrpsee_example/api/tests/crud_tests.rs b/examples/jsonrpsee_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..758c345cb3 --- /dev/null +++ b/examples/jsonrpsee_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use entity::post; +use jsonrpsee_example_api::service::{Mutation, Query}; +use sea_orm::Database; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/jsonrpsee_example/core/Cargo.toml b/examples/jsonrpsee_example/core/Cargo.toml deleted file mode 100644 index 0c3c04b9e7..0000000000 --- a/examples/jsonrpsee_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "jsonrpsee-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-sqlite", - # "sqlx-postgres", - # "sqlx-mysql", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/jsonrpsee_example/core/tests/mock.rs b/examples/jsonrpsee_example/core/tests/mock.rs deleted file mode 100644 index 068f31b6c8..0000000000 --- a/examples/jsonrpsee_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::post; -use jsonrpsee_example_core::{Mutation, Query}; -use prepare::prepare_mock_db; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/jsonrpsee_example/core/tests/prepare.rs b/examples/jsonrpsee_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/jsonrpsee_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/jsonrpsee_example/entity/Cargo.toml b/examples/jsonrpsee_example/entity/Cargo.toml index 7dea367337..9e429a350b 100644 --- a/examples/jsonrpsee_example/entity/Cargo.toml +++ b/examples/jsonrpsee_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,5 +13,5 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/jsonrpsee_example/entity/src/lib.rs b/examples/jsonrpsee_example/entity/src/lib.rs index e8b6291ac3..b85eeb2ad5 100644 --- a/examples/jsonrpsee_example/entity/src/lib.rs +++ b/examples/jsonrpsee_example/entity/src/lib.rs @@ -1 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub mod prelude; + pub mod post; diff --git a/examples/jsonrpsee_example/entity/src/post.rs b/examples/jsonrpsee_example/entity/src/post.rs index fbc002d188..9f1f96911d 100644 --- a/examples/jsonrpsee_example/entity/src/post.rs +++ b/examples/jsonrpsee_example/entity/src/post.rs @@ -1,8 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] -#[sea_orm(table_name = "posts")] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, @@ -11,7 +14,4 @@ pub struct Model { pub text: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/jsonrpsee_example/entity/src/prelude.rs b/examples/jsonrpsee_example/entity/src/prelude.rs new file mode 100644 index 0000000000..0840a758e9 --- /dev/null +++ b/examples/jsonrpsee_example/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub use super::post::Entity as Post; diff --git a/examples/jsonrpsee_example/migration/Cargo.toml b/examples/jsonrpsee_example/migration/Cargo.toml index 37cd12fc79..9b7dfb86c7 100644 --- a/examples/jsonrpsee_example/migration/Cargo.toml +++ b/examples/jsonrpsee_example/migration/Cargo.toml @@ -1,21 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-sqlite", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-sqlite", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/jsonrpsee_example/migration/src/lib.rs b/examples/jsonrpsee_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/jsonrpsee_example/migration/src/lib.rs +++ b/examples/jsonrpsee_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/jsonrpsee_example/migration/src/m20220120_000001_create_post_table.rs b/examples/jsonrpsee_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/jsonrpsee_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/jsonrpsee_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/jsonrpsee_example/migration/src/m20220120_000002_seed_posts.rs b/examples/jsonrpsee_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/jsonrpsee_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/jsonrpsee_example/migration/src/main.rs b/examples/jsonrpsee_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/jsonrpsee_example/migration/src/main.rs +++ b/examples/jsonrpsee_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/loco_example/.cargo/config.toml b/examples/loco_example/.cargo/config.toml new file mode 100644 index 0000000000..2def59ad76 --- /dev/null +++ b/examples/loco_example/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +loco = "run --" +playground = "run --example playground" diff --git a/examples/loco_example/.dockerignore b/examples/loco_example/.dockerignore new file mode 100644 index 0000000000..41e0a30635 --- /dev/null +++ b/examples/loco_example/.dockerignore @@ -0,0 +1,5 @@ +target +dockerfile +.dockerignore +.git +.gitignore diff --git a/examples/loco_example/.gitignore b/examples/loco_example/.gitignore new file mode 100644 index 0000000000..624c1b572f --- /dev/null +++ b/examples/loco_example/.gitignore @@ -0,0 +1,17 @@ +**/config/local.yaml +**/config/*.local.yaml + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +!Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/examples/loco_example/.rustfmt.toml b/examples/loco_example/.rustfmt.toml new file mode 100644 index 0000000000..26e84608fd --- /dev/null +++ b/examples/loco_example/.rustfmt.toml @@ -0,0 +1,7 @@ +comment_width = 80 +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +max_width = 100 +use_small_heuristics = "Default" +wrap_comments = true diff --git a/examples/loco_example/Cargo.lock b/examples/loco_example/Cargo.lock new file mode 100644 index 0000000000..123d31124b --- /dev/null +++ b/examples/loco_example/Cargo.lock @@ -0,0 +1,5693 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "axum-test" +version = "17.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum", + "bytes", + "bytesize", + "cookie", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "backon" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "backtrace_printer" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d28de81c708c843640982b66573df0f0168d87e42854b563971f326745aab7" +dependencies = [ + "btparse-stable", + "colored 2.2.0", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "btparse-stable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d75b8252ed252f881d1dc4482ae3c3854df6ee8183c1906bac50ff358f4f89f" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-unit" +version = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytesize" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" + +[[package]] +name = "cc" +version = "1.2.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom 7.1.3", + "once_cell", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cruet" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + +[[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 = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "duct" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] + +[[package]] +name = "duct_sh" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8139179d1d133ab7153920ba3812915b17c61e2514a6f98b1fd03f2c07668d1" +dependencies = [ + "duct", +] + +[[package]] +name = "ego-tree" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "english-to-cron" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26fb7377cbec9a94f60428e6e6afbe10c699a14639b4d3d4b67b25c0bbe0806" +dependencies = [ + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[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 = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.9.4", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.3", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.4", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "insta" +version = "1.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +dependencies = [ + "console", + "once_cell", + "pest", + "pest_derive", + "regex", + "serde", + "similar", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lettre" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" +dependencies = [ + "async-trait", + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "nom 8.0.0", + "percent-encoding", + "quoted_printable", + "rustls", + "socket2 0.6.0", + "tokio", + "tokio-rustls", + "url", + "webpki-roots 1.0.2", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "loco-gen" +version = "0.16.3" +source = "git+https://github.com/SeaQL/loco?branch=master#0e2860b7885e6c00e013b3618896a78095809ede" +dependencies = [ + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.14.0", + "duct", + "heck 0.4.1", + "include_dir", + "regex", + "rrgen", + "serde", + "serde_json", + "tera", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "loco-rs" +version = "0.16.3" +source = "git+https://github.com/SeaQL/loco?branch=master#0e2860b7885e6c00e013b3618896a78095809ede" +dependencies = [ + "argon2", + "async-trait", + "axum", + "axum-extra", + "axum-test", + "backtrace_printer", + "byte-unit", + "bytes", + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.13.3", + "dashmap 6.1.0", + "duct", + "duct_sh", + "english-to-cron", + "futures-util", + "heck 0.4.1", + "include_dir", + "ipnetwork", + "jsonwebtoken", + "lettre", + "loco-gen", + "moka", + "notify", + "opendal", + "rand 0.9.2", + "redis", + "regex", + "scraper", + "sea-orm", + "sea-orm-migration", + "semver", + "serde", + "serde_json", + "serde_variant", + "serde_yaml", + "sqlx", + "tera", + "thiserror 1.0.69", + "tokio", + "tokio-cron-scheduler", + "tokio-util", + "toml", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tree-fs", + "ulid", + "uuid", + "validator", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "loco-rs", + "sea-orm-migration", + "tokio", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +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.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[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 = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.4", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "opendal" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb9838d0575c6dbaf3fcec7255af8d5771996d4af900bbb6fa9a314dec00a1a" +dependencies = [ + "anyhow", + "backon", + "base64", + "bytes", + "chrono", + "futures", + "getrandom 0.2.16", + "http", + "http-body", + "log", + "md-5", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.6", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +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.106", + "version_check", + "yansi", +] + +[[package]] +name = "psm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" +dependencies = [ + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redis" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "reserve-port" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rrgen" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7a7ede035354391a37e42aa4935b3d8921f0ded896d2ce44bb1a3b6dd76bab" +dependencies = [ + "cruet 0.13.3", + "fs-err", + "glob", + "heck 0.4.1", + "regex", + "serde", + "serde_json", + "serde_regex", + "serde_yaml", + "tera", + "thiserror 1.0.69", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.9.2", + "thiserror 2.0.17", +] + +[[package]] +name = "rust_decimal" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" +dependencies = [ + "ahash 0.8.12", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "indexmap", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "880cc0a64f1ee0320d7d6cd2b9290c27e89750669cfc5b6afdce1654755a2983" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more 2.0.1", + "futures-util", + "itertools", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "2.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce69601010a0507a7e465a63bb63064984f7cfd05d04af39df70834e5713aeb6" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "indoc", + "regex", + "sea-schema", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aae91cb86cf389da96119df71967ffb1b3caf1687303def77336f8dc9d33e78" +dependencies = [ + "heck 0.5.0", + "pluralizer", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "2.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab8128cb63269debc8a88dbef5577076a5b53f8d0055d2d0939f1533154e2ef" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58df33eded14fba318338034b4bd197973fd7570a9f93ee5f88a27a7731f44f3" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217e9422de35f26c16c5f671fce3c075a65e10322068dbc66078428634af6195" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", + "thiserror 2.0.17", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68873fa1776b4c25a26e7679f8ee22332978c721168ec1b0b32b6583d5a9381d" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845b7ed3e7a4f4458fe7218931b54e92be0dce01fc3c310d996c7b76d9a37ea5" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags 2.9.4", + "cssparser", + "derive_more 0.99.20", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +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 = "serde_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap 5.5.3", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "servo_arc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.106", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.106", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.9.4", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.9.4", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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 = "todolist" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "chrono", + "eyre", + "include_dir", + "insta", + "loco-rs", + "migration", + "rstest", + "sea-orm", + "serde", + "serde_json", + "serial_test", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-cron-scheduler" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2594dd7c2abbbafbb1c78d167fd10860dc7bd75f814cb051a1e0d3e796b9702" +dependencies = [ + "chrono", + "cron", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "async-compression", + "bitflags 2.9.4", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tree-fs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6115680fa5fdb99b4ff19c9c3217e75116d2bb0eae82458c4e1818be6a10c7" +dependencies = [ + "rand 0.9.2", + "serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "rand 0.9.2", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[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.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/loco_example/Cargo.toml b/examples/loco_example/Cargo.toml new file mode 100644 index 0000000000..29beb30a35 --- /dev/null +++ b/examples/loco_example/Cargo.toml @@ -0,0 +1,50 @@ +[workspace] + +[package] +edition = "2024" +name = "todolist" +rust-version = "1.85.0" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +loco-rs = { version = "0.16" } +migration = { path = "migration" } + +async-trait = "0.1.74" +axum = "0.8" +chrono = "0.4" +eyre = "0.6" +include_dir = "0.7" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.33.0", default-features = false } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } +uuid = { version = "1.6.0", features = ["v4"] } +validator = { version = "0.20" } + +[dependencies.sea-orm] +features = [ + "sqlx-sqlite", + "sqlx-postgres", + "runtime-tokio-rustls", + "macros", +] +version = "~2.0.0-rc.37" # sea-orm version + +[[bin]] +name = "todolist-cli" +path = "src/bin/main.rs" +required-features = [] + +[dev-dependencies] +insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } +loco-rs = { version = "0.16", features = ["testing"] } +rstest = "0.18.2" +serial_test = "2.0.0" + +[patch.crates-io] +loco-rs = { git = "https://github.com/SeaQL/loco", branch = "master" } diff --git a/examples/loco_example/README.md b/examples/loco_example/README.md new file mode 100644 index 0000000000..1404899d8c --- /dev/null +++ b/examples/loco_example/README.md @@ -0,0 +1,95 @@ +![screenshot](Screenshot.png) + +> Adapted from https://github.com/loco-rs/todo-list + +# Loco with SeaORM example todo list + +Build your own todo list website using Loco. Follow the step-by-step guide [here](<(https://loco.rs/blog/frontend-website/)>) to create it effortlessly. + +## Build Client + +Navigate to the `frontend` directory and build the client: + +```sh +$ cd frontend && yarn install && yarn build + +vite v5.0.8 building for production... +✓ 120 modules transformed. +dist/index.html 0.46 kB │ gzip: 0.30 kB +dist/assets/index-AbTMZIjW.css 1.26 kB │ gzip: 0.65 kB +dist/assets/index-MJFpQvzE.js 235.64 kB │ gzip: 75.58 kB +✓ built in 2.01s +``` + +## Run Locally + +You need: + +* A local postgres instance + +Check out your development [configuration](config/development.yaml). + +> To configure a database , please run a local postgres database with loco:loco and a db named [app name]_development.: +docker run -d -p 5432:5432 -e POSTGRES_USER=loco -e POSTGRES_DB=[app name]_development -e POSTGRES_PASSWORD="loco" postgres:15.3-alpine + +Execute the following command to run your todo list website locally, serving static assets from `frontend/dist`: + +```sh +$ cargo loco start + + Finished dev [unoptimized + debuginfo] target(s) in 0.53s + Running `target/debug/todolist-cli start` +2024-02-01T08:49:41.070430Z INFO loco_rs::db: auto migrating +2024-02-01T08:49:41.073698Z INFO sea_orm_migration::migrator: Applying all pending migrations +2024-02-01T08:49:41.078191Z INFO sea_orm_migration::migrator: No pending migrations +2024-02-01T08:49:41.100557Z INFO loco_rs::controller::app_routes: [GET] /api/_ping +2024-02-01T08:49:41.100617Z INFO loco_rs::controller::app_routes: [GET] /api/_health +2024-02-01T08:49:41.100667Z INFO loco_rs::controller::app_routes: [GET] /api/notes +2024-02-01T08:49:41.100702Z INFO loco_rs::controller::app_routes: [POST] /api/notes +2024-02-01T08:49:41.100738Z INFO loco_rs::controller::app_routes: [GET] /api/notes/{id} +2024-02-01T08:49:41.100791Z INFO loco_rs::controller::app_routes: [DELETE] /api/notes/{id} +2024-02-01T08:49:41.100817Z INFO loco_rs::controller::app_routes: [POST] /api/notes/{id} +2024-02-01T08:49:41.100934Z INFO loco_rs::controller::app_routes: [Middleware] Adding limit payload data="5mb" +2024-02-01T08:49:41.101017Z INFO loco_rs::controller::app_routes: [Middleware] Adding log trace id +2024-02-01T08:49:41.101057Z INFO loco_rs::controller::app_routes: [Middleware] Adding timeout layer +2024-02-01T08:49:41.101192Z INFO loco_rs::controller::app_routes: [Middleware] Adding cors +2024-02-01T08:49:41.101241Z INFO loco_rs::controller::app_routes: [Middleware] Adding static + + ▄ ▀ + ▀ ▄ + ▄ ▀ ▄ ▄ ▄▀ + ▄ ▀▄▄ + ▄ ▀ ▀ ▀▄▀█▄ + ▀█▄ +▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ + ██████ █████ ███ █████ ███ █████ ███ ▀█ + ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ + ██████ █████ ███ █████ █████ ███ ████▄ + ██████ █████ ███ █████ ▄▄▄ █████ ███ █████ + ██████ █████ ███ ████ ███ █████ ███ ████▀ + ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + https://loco.rs + +environment: development + database: automigrate + logger: debug +compilation: debug + modes: server + +listening on port 3000 +``` + +## Development + +To develop the UI, run the following commands: + +```sh +$ cd frontend && yarn install && yarn dev +``` + +To run the server: + +```sh +$ cargo loco start +``` diff --git a/examples/loco_example/Screenshot.png b/examples/loco_example/Screenshot.png new file mode 100644 index 0000000000..3bb65b1632 Binary files /dev/null and b/examples/loco_example/Screenshot.png differ diff --git a/examples/loco_example/config/development.yaml b/examples/loco_example/config/development.yaml new file mode 100644 index 0000000000..71e0484c30 --- /dev/null +++ b/examples/loco_example/config/development.yaml @@ -0,0 +1,72 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: true + # Log level, options: trace, debug, info, warn or error. + level: debug + # Define the logging format. options: compact, pretty or Json + format: compact + # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 3000 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: true + # Duration time in milliseconds. + timeout: 5000 + cors: + enable: true + static: + enable: true + must_exist: true + folder: + uri: "/" + path: "frontend/dist" + fallback: "frontend/dist/index.html" + +# Database Configuration +database: + # Database connection URI + uri: postgres://loco:loco@localhost:5432/loco_app + # When enabled, the sql query will be logged. + enable_logging: false + # Set the timeout duration when acquiring a connection. + connect_timeout: 500 + # Set the idle duration before closing a connection. + idle_timeout: 500 + # Minimum number of connections for a pool. + min_connections: 1 + # Maximum number of connections for a pool. + max_connections: 1 + # Run migration up when application loaded + auto_migrate: true + # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_truncate: false + # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_recreate: false diff --git a/examples/loco_example/dockerfile b/examples/loco_example/dockerfile new file mode 100644 index 0000000000..4c39e47a51 --- /dev/null +++ b/examples/loco_example/dockerfile @@ -0,0 +1,18 @@ +FROM rust:1.74-slim as builder + +WORKDIR /usr/src/ + +COPY . . + +RUN cargo build --release + +FROM debian:bookworm-slim + +WORKDIR /usr/app + +COPY --from=builder /usr/src/frontend/dist /usr/app/frontend/dist +COPY --from=builder /usr/src/frontend/dist/index.html /usr/app/frontend/dist/index.html +COPY --from=builder /usr/src/config /usr/app/config +COPY --from=builder /usr/src/target/release/todolist-cli /usr/app/todolist-cli + +ENTRYPOINT ["/usr/app/todolist-cli"] \ No newline at end of file diff --git a/examples/loco_example/frontend/.eslintrc.cjs b/examples/loco_example/frontend/.eslintrc.cjs new file mode 100644 index 0000000000..4dcb43901a --- /dev/null +++ b/examples/loco_example/frontend/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + settings: { react: { version: '18.2' } }, + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/examples/loco_example/frontend/.gitignore b/examples/loco_example/frontend/.gitignore new file mode 100644 index 0000000000..950b529379 --- /dev/null +++ b/examples/loco_example/frontend/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.yarn +.yarnrc.yml +.pnp.* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/loco_example/frontend/index.html b/examples/loco_example/frontend/index.html new file mode 100644 index 0000000000..0c589eccd4 --- /dev/null +++ b/examples/loco_example/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/loco_example/frontend/package.json b/examples/loco_example/frontend/package.json new file mode 100644 index 0000000000..e33c95f82a --- /dev/null +++ b/examples/loco_example/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.6.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-query": "3.39.3", + "react-router-dom": "6.15.0" + }, + "devDependencies": { + "@types/react": "18.2.43", + "@types/react-dom": "18.2.17", + "@vitejs/plugin-react": "4.2.1", + "eslint": "8.55.0", + "eslint-plugin-react": "7.33.2", + "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-react-refresh": "0.4.5", + "vite": "5.0.8" + } +} diff --git a/examples/loco_example/frontend/pnpm-lock.yaml b/examples/loco_example/frontend/pnpm-lock.yaml new file mode 100644 index 0000000000..302e5a5d36 --- /dev/null +++ b/examples/loco_example/frontend/pnpm-lock.yaml @@ -0,0 +1,2898 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + axios: + specifier: ^1.6.2 + version: 1.6.2 + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + react-query: + specifier: 3.39.3 + version: 3.39.3(react-dom@18.2.0)(react@18.2.0) + react-router-dom: + specifier: 6.15.0 + version: 6.15.0(react-dom@18.2.0)(react@18.2.0) + +devDependencies: + '@types/react': + specifier: 18.2.43 + version: 18.2.43 + '@types/react-dom': + specifier: 18.2.17 + version: 18.2.17 + '@vitejs/plugin-react': + specifier: 4.2.1 + version: 4.2.1(vite@5.0.8) + eslint: + specifier: 8.55.0 + version: 8.55.0 + eslint-plugin-react: + specifier: 7.33.2 + version: 7.33.2(eslint@8.55.0) + eslint-plugin-react-hooks: + specifier: 4.6.0 + version: 4.6.0(eslint@8.55.0) + eslint-plugin-react-refresh: + specifier: 0.4.5 + version: 0.4.5(eslint@8.55.0) + tailwindcss: + specifier: ^3.3.6 + version: 3.3.6 + vite: + specifier: 5.0.8 + version: 5.0.8 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.6: + resolution: {integrity: sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.6) + '@babel/helpers': 7.23.6 + '@babel/parser': 7.23.6 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.6 + '@babel/types': 7.23.6 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.22.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.6): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.6 + '@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 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.23.6: + resolution: {integrity: sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.6 + '@babel/types': 7.23.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.6): + resolution: {integrity: sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.23.3(@babel/core@7.23.6): + resolution: {integrity: sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/runtime@7.23.6: + resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@babel/traverse@7.23.6: + resolution: {integrity: sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.6: + resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@esbuild/android-arm64@0.19.9: + resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.9: + resolution: {integrity: sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.9: + resolution: {integrity: sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.9: + resolution: {integrity: sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.9: + resolution: {integrity: sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.9: + resolution: {integrity: sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.9: + resolution: {integrity: sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.9: + resolution: {integrity: sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.9: + resolution: {integrity: sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.9: + resolution: {integrity: sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.9: + resolution: {integrity: sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.9: + resolution: {integrity: sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.9: + resolution: {integrity: sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.9: + resolution: {integrity: sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.9: + resolution: {integrity: sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.9: + resolution: {integrity: sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.9: + resolution: {integrity: sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.9: + resolution: {integrity: sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.9: + resolution: {integrity: sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.9: + resolution: {integrity: sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.9: + resolution: {integrity: sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.9: + resolution: {integrity: sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.55.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.55.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.55.0: + resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@remix-run/router@1.8.0: + resolution: {integrity: sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==} + engines: {node: '>=14.0.0'} + dev: false + + /@rollup/rollup-android-arm-eabi@4.9.0: + resolution: {integrity: sha512-+1ge/xmaJpm1KVBuIH38Z94zj9fBD+hp+/5WLaHgyY8XLq1ibxk/zj6dTXaqM2cAbYKq8jYlhHd6k05If1W5xA==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.9.0: + resolution: {integrity: sha512-im6hUEyQ7ZfoZdNvtwgEJvBWZYauC9KVKq1w58LG2Zfz6zMd8gRrbN+xCVoqA2hv/v6fm9lp5LFGJ3za8EQH3A==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.9.0: + resolution: {integrity: sha512-u7aTMskN6Dmg1lCT0QJ+tINRt+ntUrvVkhbPfFz4bCwRZvjItx2nJtwJnJRlKMMaQCHRjrNqHRDYvE4mBm3DlQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.9.0: + resolution: {integrity: sha512-8FvEl3w2ExmpcOmX5RJD0yqXcVSOqAJJUJ29Lca29Ik+3zPS1yFimr2fr5JSZ4Z5gt8/d7WqycpgkX9nocijSw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.9.0: + resolution: {integrity: sha512-lHoKYaRwd4gge+IpqJHCY+8Vc3hhdJfU6ukFnnrJasEBUvVlydP8PuwndbWfGkdgSvZhHfSEw6urrlBj0TSSfg==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.9.0: + resolution: {integrity: sha512-JbEPfhndYeWHfOSeh4DOFvNXrj7ls9S/2omijVsao+LBPTPayT1uKcK3dHW3MwDJ7KO11t9m2cVTqXnTKpeaiw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.9.0: + resolution: {integrity: sha512-ahqcSXLlcV2XUBM3/f/C6cRoh7NxYA/W7Yzuv4bDU1YscTFw7ay4LmD7l6OS8EMhTNvcrWGkEettL1Bhjf+B+w==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.9.0: + resolution: {integrity: sha512-uwvOYNtLw8gVtrExKhdFsYHA/kotURUmZYlinH2VcQxNCQJeJXnkmWgw2hI9Xgzhgu7J9QvWiq9TtTVwWMDa+w==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.9.0: + resolution: {integrity: sha512-m6pkSwcZZD2LCFHZX/zW2aLIISyzWLU3hrLLzQKMI12+OLEzgruTovAxY5sCZJkipklaZqPy/2bEEBNjp+Y7xg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.9.0: + resolution: {integrity: sha512-VFAC1RDRSbU3iOF98X42KaVicAfKf0m0OvIu8dbnqhTe26Kh6Ym9JrDulz7Hbk7/9zGc41JkV02g+p3BivOdAg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.9.0: + resolution: {integrity: sha512-9jPgMvTKXARz4inw6jezMLA2ihDBvgIU9Ml01hjdVpOcMKyxFBJrn83KVQINnbeqDv0+HdO1c09hgZ8N0s820Q==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.9.0: + resolution: {integrity: sha512-WE4pT2kTXQN2bAv40Uog0AsV7/s9nT9HBWXAou8+++MBCnY51QS02KYtm6dQxxosKi1VIz/wZIrTQO5UP2EW+Q==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.9.0: + resolution: {integrity: sha512-aPP5Q5AqNGuT0tnuEkK/g4mnt3ZhheiXrDIiSVIHN9mcN21OyXDVbEMqmXPE7e2OplNLDkcvV+ZoGJa2ZImFgw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + '@types/babel__generator': 7.6.7 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.4 + dev: true + + /@types/babel__generator@7.6.7: + resolution: {integrity: sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@types/babel__traverse@7.20.4: + resolution: {integrity: sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: true + + /@types/react-dom@18.2.17: + resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} + dependencies: + '@types/react': 18.2.43 + dev: true + + /@types/react@18.2.43: + resolution: {integrity: sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==} + dependencies: + '@types/prop-types': 15.7.11 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + dev: true + + /@types/scheduler@0.16.8: + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.2.1(vite@5.0.8): + resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + dependencies: + '@babel/core': 7.23.6 + '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.6) + '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.0 + vite: 5.0.8 + transitivePeerDependencies: + - supports-color + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.2 + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + dev: true + + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-string: 1.0.7 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.tosorted@1.1.2: + resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /asynciterator.prototype@1.0.0: + resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} + dependencies: + has-symbols: 1.0.3 + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} + dependencies: + follow-redirects: 1.15.3 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /broadcast-channel@3.7.0: + resolution: {integrity: sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==} + dependencies: + '@babel/runtime': 7.23.6 + detect-node: 2.1.0 + js-sha3: 0.8.0 + microseconds: 0.2.0 + nano-time: 1.0.0 + oblivious-set: 1.0.0 + rimraf: 3.0.2 + unload: 2.2.0 + dev: false + + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001570 + electron-to-chromium: 1.4.612 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) + dev: true + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /caniuse-lite@1.0.30001570: + resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + dev: false + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /electron-to-chromium@1.4.612: + resolution: {integrity: sha512-dM8BMtXtlH237ecSMnYdYuCkib2QHq0kpWfUnavjdYsyr/6OsAwg5ZGUfnQ9KD1Ga4QgB2sqXlB2NT8zy2GnVg==} + dev: true + + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: true + + /es-iterator-helpers@1.0.15: + resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} + dependencies: + asynciterator.prototype: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-set-tostringtag: 2.0.2 + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + globalthis: 1.0.3 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.6 + iterator.prototype: 1.1.2 + safe-array-concat: 1.0.1 + dev: true + + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.0 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.19.9: + resolution: {integrity: sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.9 + '@esbuild/android-arm64': 0.19.9 + '@esbuild/android-x64': 0.19.9 + '@esbuild/darwin-arm64': 0.19.9 + '@esbuild/darwin-x64': 0.19.9 + '@esbuild/freebsd-arm64': 0.19.9 + '@esbuild/freebsd-x64': 0.19.9 + '@esbuild/linux-arm': 0.19.9 + '@esbuild/linux-arm64': 0.19.9 + '@esbuild/linux-ia32': 0.19.9 + '@esbuild/linux-loong64': 0.19.9 + '@esbuild/linux-mips64el': 0.19.9 + '@esbuild/linux-ppc64': 0.19.9 + '@esbuild/linux-riscv64': 0.19.9 + '@esbuild/linux-s390x': 0.19.9 + '@esbuild/linux-x64': 0.19.9 + '@esbuild/netbsd-x64': 0.19.9 + '@esbuild/openbsd-x64': 0.19.9 + '@esbuild/sunos-x64': 0.19.9 + '@esbuild/win32-arm64': 0.19.9 + '@esbuild/win32-ia32': 0.19.9 + '@esbuild/win32-x64': 0.19.9 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.55.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.55.0 + dev: true + + /eslint-plugin-react-refresh@0.4.5(eslint@8.55.0): + resolution: {integrity: sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.55.0 + dev: true + + /eslint-plugin-react@7.33.2(eslint@8.55.0): + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.7 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.2 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.15 + eslint: 8.55.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.7 + object.fromentries: 2.0.7 + object.hasown: 1.1.3 + object.values: 1.1.7 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.10 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.55.0: + resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.55.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.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + 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.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.2 + acorn-jsx: 5.3.2(acorn@8.11.2) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + + /is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + dependencies: + call-bind: 1.0.5 + dev: true + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: true + + /is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.5 + dev: true + + /is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /iterator.prototype@1.1.2: + resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.4 + set-function-name: 2.0.1 + dev: true + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: true + + /js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.7 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.1.7 + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /match-sorter@6.3.1: + resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==} + dependencies: + '@babel/runtime': 7.23.6 + remove-accents: 0.4.2 + dev: false + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /microseconds@0.2.0: + resolution: {integrity: sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nano-time@1.0.0: + resolution: {integrity: sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==} + dependencies: + big-integer: 1.6.52 + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.hasown@1.1.3: + resolution: {integrity: sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /oblivious-set@1.0.0: + resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /postcss-import@15.1.0(postcss@8.4.32): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.32 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.32): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.32 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.32): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + postcss: 8.4.32 + yaml: 2.3.4 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.32): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.32 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.32: + resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: true + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: true + + /react-query@3.39.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.6 + broadcast-channel: 3.7.0 + match-sorter: 6.3.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react-router-dom@6.15.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.8.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.15.0(react@18.2.0) + dev: false + + /react-router@6.15.0(react@18.2.0): + resolution: {integrity: sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.8.0 + react: 18.2.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /reflect.getprototypeof@1.0.4: + resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + globalthis: 1.0.3 + which-builtin-type: 1.1.3 + dev: true + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: false + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + + /remove-accents@0.4.2: + resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + + /rollup@4.9.0: + resolution: {integrity: sha512-bUHW/9N21z64gw8s6tP4c88P382Bq/L5uZDowHlHx6s/QWpjJXivIAbEw6LZthgSvlEizZBfLC4OAvWe7aoF7A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.9.0 + '@rollup/rollup-android-arm64': 4.9.0 + '@rollup/rollup-darwin-arm64': 4.9.0 + '@rollup/rollup-darwin-x64': 4.9.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.9.0 + '@rollup/rollup-linux-arm64-gnu': 4.9.0 + '@rollup/rollup-linux-arm64-musl': 4.9.0 + '@rollup/rollup-linux-riscv64-gnu': 4.9.0 + '@rollup/rollup-linux-x64-gnu': 4.9.0 + '@rollup/rollup-linux-x64-musl': 4.9.0 + '@rollup/rollup-win32-arm64-msvc': 4.9.0 + '@rollup/rollup-win32-ia32-msvc': 4.9.0 + '@rollup/rollup-win32-x64-msvc': 4.9.0 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-regex: 1.1.4 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.1 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /string.prototype.matchall@4.0.10: + resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + internal-slot: 1.0.6 + regexp.prototype.flags: 1.5.1 + set-function-name: 2.0.1 + side-channel: 1.0.4 + dev: true + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tailwindcss@3.3.6: + resolution: {integrity: sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==} + engines: {node: '>=14.0.0'} + hasBin: 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.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + 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.32 + postcss-import: 15.1.0(postcss@8.4.32) + postcss-js: 4.0.1(postcss@8.4.32) + postcss-load-config: 4.0.2(postcss@8.4.32) + postcss-nested: 6.0.1(postcss@8.4.32) + postcss-selector-parser: 6.0.13 + resolve: 1.22.8 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /unload@2.2.0: + resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==} + dependencies: + '@babel/runtime': 7.23.6 + detect-node: 2.1.0 + dev: false + + /update-browserslist-db@1.0.13(browserslist@4.22.2): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.2 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /vite@5.0.8: + resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.19.9 + postcss: 8.4.32 + rollup: 4.9.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-builtin-type@1.1.3: + resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} + engines: {node: '>= 0.4'} + dependencies: + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.0 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.13 + dev: true + + /which-collection@1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: true + + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/examples/loco_example/frontend/src/App.css b/examples/loco_example/frontend/src/App.css new file mode 100644 index 0000000000..791974a435 --- /dev/null +++ b/examples/loco_example/frontend/src/App.css @@ -0,0 +1,37 @@ +#root { + max-width: 1280px; + margin: 0 auto; + text-align: center; +} + +.logo { + height: 10em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #ff1111aa); +} + +.todo-list { + text-align: left; +} + +.todo-list .todo{ + margin-top: 10px; +} + +.todo-add { + margin-top: 20px; + text-align: left; + +} + +.todo-add input{ + width: 70%; + height: 30px; +} +.todo-add button{ + float: right; +} \ No newline at end of file diff --git a/examples/loco_example/frontend/src/App.jsx b/examples/loco_example/frontend/src/App.jsx new file mode 100644 index 0000000000..7d7933b554 --- /dev/null +++ b/examples/loco_example/frontend/src/App.jsx @@ -0,0 +1,163 @@ +import { + useQuery, + useMutation, + useQueryClient, +} from 'react-query' +import axios from 'axios'; +import './App.css' +import { Routes, Route, Outlet, Link } from "react-router-dom"; + +import { useState } from "react"; + +export default function App() { + + return ( +
+

Loco Todo List

+ + }> + } /> + } /> + + +
+ ) +} + + +function Layout() { + return ( +
+
+ + Loco logo + +
+
+ +
+ ); +} + +function TodoList() { + + const queryClient = useQueryClient(); + + const fetchTodos = async () => { + const { data } = await axios.get(`api/notes`) + return data; + } + + const { isLoading, isError, data = [] } = useQuery(["todos"], fetchTodos); // a hook provided by react-query, it takes a key(name) and function that returns a promise + + + const remove = async (id) => { + try { + const response = await axios.delete(`api/notes/${id}`); + return response.data; + } catch (error) { + console.error('Error posting todo:', error); + throw error; + } + }; + + + const mutation = useMutation(remove, { + onSuccess: () => { + queryClient.invalidateQueries(["todos"]); + }, + }); + + console.log(data) + if (isLoading) + return ( +
+

isLoading...

+
+ ); + + if (isError) + return ( +
+

Could not get todo list from the server

+
+ ); + + + return ( +
+ +
+ {data.map((todo) => ( +
+
+
{todo.title}
+
+ +
+ ))} +
+
+ ); +} + + +function AddTodo() { + const [todo, setTodo] = useState(""); + const queryClient = useQueryClient(); + + const add = async (newTodo) => { + try { + const response = await axios.post(`api/notes`, { + title: newTodo, + content: newTodo, + }); + return response.data; + } catch (error) { + console.error('Error posting todo:', error); + throw error; + } + }; + + + const mutation = useMutation(add, { + onSuccess: () => { + setTodo("") + queryClient.invalidateQueries(["todos"]); + }, + }); + + return ( +
+ { + setTodo(event.target.value); + }} + type="text" + /> + +
+ ); +} + +function NoMatch() { + return ( +
+

Sorry, this page not found

+

+ Go to the todo list page +

+
+ ); +} diff --git a/examples/loco_example/frontend/src/assets/loco.svg b/examples/loco_example/frontend/src/assets/loco.svg new file mode 100644 index 0000000000..ea229494f4 --- /dev/null +++ b/examples/loco_example/frontend/src/assets/loco.svg @@ -0,0 +1,5116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/loco_example/frontend/src/index.css b/examples/loco_example/frontend/src/index.css new file mode 100644 index 0000000000..6119ad9a8f --- /dev/null +++ b/examples/loco_example/frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/loco_example/frontend/src/main.jsx b/examples/loco_example/frontend/src/main.jsx new file mode 100644 index 0000000000..008108079a --- /dev/null +++ b/examples/loco_example/frontend/src/main.jsx @@ -0,0 +1,21 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from "react-router-dom"; +import App from './App.jsx' +import './index.css' +import { + QueryClient, + QueryClientProvider, +} from 'react-query' + +const queryClient = new QueryClient(); + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + + + +) diff --git a/examples/loco_example/frontend/vite.config.js b/examples/loco_example/frontend/vite.config.js new file mode 100644 index 0000000000..c56ce4f248 --- /dev/null +++ b/examples/loco_example/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://127.0.0.1:3000", + changeOrigin: true, + secure: false, + }, + }, + }, +}) diff --git a/examples/loco_example/frontend/yarn.lock b/examples/loco_example/frontend/yarn.lock new file mode 100644 index 0000000000..d4ee278698 --- /dev/null +++ b/examples/loco_example/frontend/yarn.lock @@ -0,0 +1,3979 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@aashutoshrathi/word-wrap@npm:^1.2.3": + version: 1.2.6 + resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" + checksum: ada901b9e7c680d190f1d012c84217ce0063d8f5c5a7725bb91ec3c5ed99bb7572680eb2d2938a531ccbaec39a95422fcd8a6b4a13110c7d98dd75402f66a0cd + languageName: node + linkType: hard + +"@ampproject/remapping@npm:^2.2.0": + version: 2.2.1 + resolution: "@ampproject/remapping@npm:2.2.1" + dependencies: + "@jridgewell/gen-mapping": ^0.3.0 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: 03c04fd526acc64a1f4df22651186f3e5ef0a9d6d6530ce4482ec9841269cf7a11dbb8af79237c282d721c5312024ff17529cd72cc4768c11e999b58e2302079 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/code-frame@npm:7.23.5" + dependencies: + "@babel/highlight": ^7.23.4 + chalk: ^2.4.2 + checksum: d90981fdf56a2824a9b14d19a4c0e8db93633fd488c772624b4e83e0ceac6039a27cd298a247c3214faa952bf803ba23696172ae7e7235f3b97f43ba278c569a + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 06ce244cda5763295a0ea924728c09bae57d35713b675175227278896946f922a63edf803c322f855a3878323d48d0255a2a3023409d2a123483c8a69ebb4744 + languageName: node + linkType: hard + +"@babel/core@npm:^7.23.5": + version: 7.23.9 + resolution: "@babel/core@npm:7.23.9" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helpers": ^7.23.9 + "@babel/parser": ^7.23.9 + "@babel/template": ^7.23.9 + "@babel/traverse": ^7.23.9 + "@babel/types": ^7.23.9 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: 634a511f74db52a5f5a283c1121f25e2227b006c095b84a02a40a9213842489cd82dc7d61cdc74e10b5bcd9bb0a4e28bab47635b54c7e2256d47ab57356e2a76 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/generator@npm:7.23.6" + dependencies: + "@babel/types": ^7.23.6 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 1a1a1c4eac210f174cd108d479464d053930a812798e09fee069377de39a893422df5b5b146199ead7239ae6d3a04697b45fc9ac6e38e0f6b76374390f91fc6c + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/helper-compilation-targets@npm:7.23.6" + dependencies: + "@babel/compat-data": ^7.23.5 + "@babel/helper-validator-option": ^7.23.5 + browserslist: ^4.22.2 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: c630b98d4527ac8fe2c58d9a06e785dfb2b73ec71b7c4f2ddf90f814b5f75b547f3c015f110a010fd31f76e3864daaf09f3adcd2f6acdbfb18a8de3a48717590 + languageName: node + linkType: hard + +"@babel/helper-environment-visitor@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 + languageName: node + linkType: hard + +"@babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" + dependencies: + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 + languageName: node + linkType: hard + +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" + dependencies: + "@babel/types": ^7.22.15 + checksum: ecd7e457df0a46f889228f943ef9b4a47d485d82e030676767e6a2fdcbdaa63594d8124d4b55fd160b41c201025aec01fc27580352b1c87a37c9c6f33d116702 + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/helper-module-transforms@npm:7.23.3" + 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 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 5d0895cfba0e16ae16f3aa92fee108517023ad89a855289c4eb1d46f7aef4519adf8e6f971e1d55ac20c5461610e17213f1144097a8f932e768a9132e2278d71 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 + languageName: node + linkType: hard + +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2 + languageName: node + linkType: hard + +"@babel/helper-split-export-declaration@npm:^7.22.6": + version: 7.22.6 + resolution: "@babel/helper-split-export-declaration@npm:7.22.6" + dependencies: + "@babel/types": ^7.22.5 + checksum: e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/helper-string-parser@npm:7.23.4" + checksum: c0641144cf1a7e7dc93f3d5f16d5327465b6cf5d036b48be61ecba41e1eece161b48f46b7f960951b67f8c3533ce506b16dece576baef4d8b3b49f8c65410f90 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-validator-option@npm:7.23.5" + checksum: 537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/helpers@npm:7.23.9" + dependencies: + "@babel/template": ^7.23.9 + "@babel/traverse": ^7.23.9 + "@babel/types": ^7.23.9 + checksum: 2678231192c0471dbc2fc403fb19456cc46b1afefcfebf6bc0f48b2e938fdb0fef2e0fe90c8c8ae1f021dae5012b700372e4b5d15867f1d7764616532e4a6324 + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/highlight@npm:7.23.4" + dependencies: + "@babel/helper-validator-identifier": ^7.22.20 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + checksum: 643acecdc235f87d925979a979b539a5d7d1f31ae7db8d89047269082694122d11aa85351304c9c978ceeb6d250591ccadb06c366f358ccee08bb9c122476b89 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/parser@npm:7.23.9" + bin: + parser: ./bin/babel-parser.js + checksum: e7cd4960ac8671774e13803349da88d512f9292d7baa952173260d3e8f15620a28a3701f14f709d769209022f9e7b79965256b8be204fc550cfe783cdcabe7c7 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-self@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 882bf56bc932d015c2d83214133939ddcf342e5bcafa21f1a93b19f2e052145115e1e0351730897fd66e5f67cad7875b8a8d81ceb12b6e2a886ad0102cb4eb1f + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-source@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 92287fb797e522d99bdc77eaa573ce79ff0ad9f1cf4e7df374645e28e51dce0adad129f6f075430b129b5bac8dad843f65021970e12e992d6d6671f0d65bb1e0 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2": + version: 7.23.9 + resolution: "@babel/runtime@npm:7.23.9" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 6bbebe8d27c0c2dd275d1ac197fc1a6c00e18dab68cc7aaff0adc3195b45862bae9c4cc58975629004b0213955b2ed91e99eccb3d9b39cabea246c657323d667 + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/template@npm:7.23.9" + dependencies: + "@babel/code-frame": ^7.23.5 + "@babel/parser": ^7.23.9 + "@babel/types": ^7.23.9 + checksum: 6e67414c0f7125d7ecaf20c11fab88085fa98a96c3ef10da0a61e962e04fdf3a18a496a66047005ddd1bb682a7cc7842d556d1db2f3f3f6ccfca97d5e445d342 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/traverse@npm:7.23.9" + dependencies: + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.23.9 + "@babel/types": ^7.23.9 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: a932f7aa850e158c00c97aad22f639d48c72805c687290f6a73e30c5c4957c07f5d28310c9bf59648e2980fe6c9d16adeb2ff92a9ca0f97fa75739c1328fc6c3 + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.8.3": + version: 7.23.9 + resolution: "@babel/types@npm:7.23.9" + dependencies: + "@babel/helper-string-parser": ^7.23.4 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 0a9b008e9bfc89beb8c185e620fa0f8ed6c771f1e1b2e01e1596870969096fec7793898a1d64a035176abf1dd13e2668ee30bf699f2d92c210a8128f4b151e65 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/aix-ppc64@npm:0.19.12" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/android-arm64@npm:0.19.12" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/android-arm@npm:0.19.12" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/android-x64@npm:0.19.12" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/darwin-arm64@npm:0.19.12" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/darwin-x64@npm:0.19.12" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/freebsd-arm64@npm:0.19.12" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/freebsd-x64@npm:0.19.12" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-arm64@npm:0.19.12" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-arm@npm:0.19.12" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-ia32@npm:0.19.12" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-loong64@npm:0.19.12" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-mips64el@npm:0.19.12" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-ppc64@npm:0.19.12" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-riscv64@npm:0.19.12" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-s390x@npm:0.19.12" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/linux-x64@npm:0.19.12" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/netbsd-x64@npm:0.19.12" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/openbsd-x64@npm:0.19.12" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/sunos-x64@npm:0.19.12" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/win32-arm64@npm:0.19.12" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/win32-ia32@npm:0.19.12" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.19.12": + version: 0.19.12 + resolution: "@esbuild/win32-x64@npm:0.19.12" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.6.1": + version: 4.10.0 + resolution: "@eslint-community/regexpp@npm:4.10.0" + checksum: 2a6e345429ea8382aaaf3a61f865cae16ed44d31ca917910033c02dc00d505d939f10b81e079fa14d43b51499c640138e153b7e40743c4c094d9df97d4e56f7b + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + 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 + checksum: 10957c7592b20ca0089262d8c2a8accbad14b4f6507e35416c32ee6b4dbf9cad67dfb77096bbd405405e9ada2b107f3797fe94362e1c55e0b09d6e90dd149127 + languageName: node + linkType: hard + +"@eslint/js@npm:8.55.0": + version: 8.55.0 + resolution: "@eslint/js@npm:8.55.0" + checksum: fa33ef619f0646ed15649b0c2e313e4d9ccee8425884bdbfc78020d6b6b64c0c42fa9d83061d0e6158e1d4274f03f0f9008786540e2efab8fcdc48082259908c + languageName: node + linkType: hard + +"@humanwhocodes/config-array@npm:^0.11.13": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" + dependencies: + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 + minimatch: ^3.0.5 + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 + languageName: node + linkType: hard + +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" + dependencies: + "@jridgewell/set-array": ^1.0.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: f5b441fe7900eab4f9155b3b93f9800a916257f4e8563afbcd3b5a5337b55e52bd8ae6735453b1b745457d9f6cdb16d74cd6220bbdd98cf153239e13f6cbb653 + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.0.1": + version: 1.1.2 + resolution: "@jridgewell/set-array@npm:1.1.2" + checksum: 69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.22 + resolution: "@jridgewell/trace-mapping@npm:0.3.22" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: ac7dd2cfe0b479aa1b81776d40d789243131cc792dc8b6b6a028c70fcd6171958ae1a71bf67b618ffe3c0c3feead9870c095ee46a5e30319410d92976b28f498 + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": 2.0.5 + run-parallel: ^1.1.9 + checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59 + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.8": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": 2.1.5 + fastq: ^1.6.0 + checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.0 + resolution: "@npmcli/agent@npm:2.2.0" + dependencies: + agent-base: ^7.1.0 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.1 + lru-cache: ^10.0.1 + socks-proxy-agent: ^8.0.1 + checksum: 3b25312edbdfaa4089af28e2d423b6f19838b945e47765b0c8174c1395c79d43c3ad6d23cb364b43f59fd3acb02c93e3b493f72ddbe3dfea04c86843a7311fc4 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: ^7.3.5 + checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + +"@remix-run/router@npm:1.8.0": + version: 1.8.0 + resolution: "@remix-run/router@npm:1.8.0" + checksum: f754f02d3b4fc86791b88acf16065000609e2324b9436027844a76831c7107c0994067cb83abdd6093c282bd518a5c89b5e02aead585782978586e3a04534428 + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.9.6" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-android-arm64@npm:4.9.6" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-darwin-arm64@npm:4.9.6" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-darwin-x64@npm:4.9.6" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.9.6" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.9.6" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.9.6" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.9.6" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.9.6" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.9.6" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.9.6" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.9.6" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.9.6": + version: 4.9.6 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.9.6" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.20.5": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: a3226f7930b635ee7a5e72c8d51a357e799d19cbf9d445710fa39ab13804f79ab1a54b72ea7d8e504659c7dfc50675db974b526142c754398d7413aa4bc30845 + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.8 + resolution: "@types/babel__generator@npm:7.6.8" + dependencies: + "@babel/types": ^7.0.0 + checksum: 5b332ea336a2efffbdeedb92b6781949b73498606ddd4205462f7d96dafd45ff3618770b41de04c4881e333dd84388bfb8afbdf6f2764cbd98be550d85c6bb48 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.4 + resolution: "@types/babel__template@npm:7.4.4" + dependencies: + "@babel/parser": ^7.1.0 + "@babel/types": ^7.0.0 + checksum: d7a02d2a9b67e822694d8e6a7ddb8f2b71a1d6962dfd266554d2513eefbb205b33ca71a0d163b1caea3981ccf849211f9964d8bd0727124d18ace45aa6c9ae29 + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*": + version: 7.20.5 + resolution: "@types/babel__traverse@npm:7.20.5" + dependencies: + "@babel/types": ^7.20.7 + checksum: 608e0ab4fc31cd47011d98942e6241b34d461608c0c0e153377c5fd822c436c475f1ded76a56bfa76a1adf8d9266b727bbf9bfac90c4cb152c97f30dadc5b7e8 + languageName: node + linkType: hard + +"@types/estree@npm:1.0.5": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a + languageName: node + linkType: hard + +"@types/prop-types@npm:*": + version: 15.7.11 + resolution: "@types/prop-types@npm:15.7.11" + checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 + languageName: node + linkType: hard + +"@types/react-dom@npm:18.2.17": + version: 18.2.17 + resolution: "@types/react-dom@npm:18.2.17" + dependencies: + "@types/react": "*" + checksum: 7a4e704ed4be6e0c3ccd8a22ff69386fe548304bf4db090513f42e059ff4c65f7a427790320051524d6578a2e4c9667bb7a80a4c989b72361c019fbe851d9385 + languageName: node + linkType: hard + +"@types/react@npm:*": + version: 18.2.50 + resolution: "@types/react@npm:18.2.50" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: f7f5a5d0b73ddf5ec574e691039197ee91bf0addbbffa408ad2a33e4d085be5c8821758220ec64a214bd2aec42ef67fc4a7f322d1f81e87b98430d9510374b45 + languageName: node + linkType: hard + +"@types/react@npm:18.2.43": + version: 18.2.43 + resolution: "@types/react@npm:18.2.43" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: e7e4da18c7b14e4b8b1ea630d1d8224835698f2bb2485766455bfb9dea93b8ee6cc549a9fe1cb10c984e62685db44fcfb61e8fac913b008cd30a92087f25b9df + languageName: node + linkType: hard + +"@types/scheduler@npm:*": + version: 0.16.8 + resolution: "@types/scheduler@npm:0.16.8" + checksum: 6c091b096daa490093bf30dd7947cd28e5b2cd612ec93448432b33f724b162587fed9309a0acc104d97b69b1d49a0f3fc755a62282054d62975d53d7fd13472d + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + +"@vitejs/plugin-react@npm:4.2.1": + version: 4.2.1 + resolution: "@vitejs/plugin-react@npm:4.2.1" + dependencies: + "@babel/core": ^7.23.5 + "@babel/plugin-transform-react-jsx-self": ^7.23.3 + "@babel/plugin-transform-react-jsx-source": ^7.23.3 + "@types/babel__core": ^7.20.5 + react-refresh: ^0.14.0 + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + checksum: 08d227d27ff2304e395e746bd2d4b5fee40587f69d7e2fcd6beb7d91163c1f1dc26d843bc48e2ffb8f38c6b8a1b9445fb07840e3dcc841f97b56bbb8205346aa + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 0e994ad2aa6575f94670d8a2149afe94465de9cedaaaac364e7fb43a40c3691c980ff74899f682f4ca58fa96b4cbd7421a015d3a6defe43a442117d7821a2f36 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: c3d3b2a89c9a056b205b69530a37b972b404ee46ec8e5b341666f9513d3163e2a4f214a71f4dfc7370f5a9c07472d2fd1c11c91c3f03d093e37637d95da98950 + languageName: node + linkType: hard + +"acorn@npm:^8.9.0": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" + bin: + acorn: bin/acorn + checksum: 76d8e7d559512566b43ab4aadc374f11f563f0a9e21626dd59cb2888444e9445923ae9f3699972767f18af61df89cd89f5eaaf772d1327b055b45cb829b4a88c + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" + dependencies: + debug: ^4.3.4 + checksum: f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: ^2.0.0 + indent-string: ^4.0.0 + checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 + languageName: node + linkType: hard + +"ajv@npm:^6.12.4": + version: 6.12.6 + resolution: "ajv@npm:6.12.6" + 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 + checksum: 874972efe5c4202ab0a68379481fbd3d1b5d0a7bd6d3cc21d40d3536ebff3352a2a1fabb632d4fd2cc7fe4cbdcd5ed6782084c9bbf7f32a1536d18f9da5007d4 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: ^1.9.0 + checksum: d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: ^2.0.1 + checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 83644b56493e89a254bae05702abf3a1101b4fa4d0ca31df1c9985275a5a5bd47b3c27b7fa0b71098d41114d8ca000e6ed90cad764b306f8a503665e4d517ced + languageName: node + linkType: hard + +"array-buffer-byte-length@npm:^1.0.0": + version: 1.0.0 + resolution: "array-buffer-byte-length@npm:1.0.0" + dependencies: + call-bind: ^1.0.2 + is-array-buffer: ^3.0.1 + checksum: 044e101ce150f4804ad19c51d6c4d4cfa505c5b2577bd179256e4aa3f3f6a0a5e9874c78cd428ee566ac574c8a04d7ce21af9fe52e844abfdccb82b33035a7c3 + languageName: node + linkType: hard + +"array-includes@npm:^3.1.6": + version: 3.1.7 + resolution: "array-includes@npm:3.1.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + get-intrinsic: ^1.2.1 + is-string: ^1.0.7 + checksum: 06f9e4598fac12a919f7c59a3f04f010ea07f0b7f0585465ed12ef528a60e45f374e79d1bddbb34cdd4338357d00023ddbd0ac18b0be36964f5e726e8965d7fc + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.3.1": + version: 1.3.2 + resolution: "array.prototype.flat@npm:1.3.2" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + es-shim-unscopables: ^1.0.0 + checksum: 5d6b4bf102065fb3f43764bfff6feb3295d372ce89591e6005df3d0ce388527a9f03c909af6f2a973969a4d178ab232ffc9236654149173e0e187ec3a1a6b87b + languageName: node + linkType: hard + +"array.prototype.flatmap@npm:^1.3.1": + version: 1.3.2 + resolution: "array.prototype.flatmap@npm:1.3.2" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + es-shim-unscopables: ^1.0.0 + checksum: ce09fe21dc0bcd4f30271f8144083aa8c13d4639074d6c8dc82054b847c7fc9a0c97f857491f4da19d4003e507172a78f4bcd12903098adac8b9cd374f734be3 + languageName: node + linkType: hard + +"array.prototype.tosorted@npm:^1.1.1": + version: 1.1.2 + resolution: "array.prototype.tosorted@npm:1.1.2" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + es-shim-unscopables: ^1.0.0 + get-intrinsic: ^1.2.1 + checksum: 3607a7d6b117f0ffa6f4012457b7af0d47d38cf05e01d50e09682fd2fb782a66093a5e5fbbdbad77c8c824794a9d892a51844041641f719ad41e3a974f0764de + languageName: node + linkType: hard + +"arraybuffer.prototype.slice@npm:^1.0.2": + version: 1.0.2 + resolution: "arraybuffer.prototype.slice@npm:1.0.2" + dependencies: + array-buffer-byte-length: ^1.0.0 + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + get-intrinsic: ^1.2.1 + is-array-buffer: ^3.0.2 + is-shared-array-buffer: ^1.0.2 + checksum: c200faf437786f5b2c80d4564ff5481c886a16dee642ef02abdc7306c7edd523d1f01d1dd12b769c7eb42ac9bc53874510db19a92a2c035c0f6696172aafa5d3 + languageName: node + linkType: hard + +"asynciterator.prototype@npm:^1.0.0": + version: 1.0.0 + resolution: "asynciterator.prototype@npm:1.0.0" + dependencies: + has-symbols: ^1.0.3 + checksum: e8ebfd9493ac651cf9b4165e9d64030b3da1d17181bb1963627b59e240cdaf021d9b59d44b827dc1dde4e22387ec04c2d0f8720cf58a1c282e34e40cc12721b3 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be + languageName: node + linkType: hard + +"available-typed-arrays@npm:^1.0.5": + version: 1.0.6 + resolution: "available-typed-arrays@npm:1.0.6" + checksum: 8295571eb86447138adf64a0df0c08ae61250b17190bba30e1fae8c80a816077a6d028e5506f602c382c0197d3080bae131e92e331139d55460989580eeae659 + languageName: node + linkType: hard + +"axios@npm:^1.6.2": + version: 1.6.7 + resolution: "axios@npm:1.6.7" + dependencies: + follow-redirects: ^1.15.4 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: 87d4d429927d09942771f3b3a6c13580c183e31d7be0ee12f09be6d5655304996bb033d85e54be81606f4e89684df43be7bf52d14becb73a12727bf33298a082 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"big-integer@npm:^1.6.16": + version: 1.6.52 + resolution: "big-integer@npm:1.6.52" + checksum: 6e86885787a20fed96521958ae9086960e4e4b5e74d04f3ef7513d4d0ad631a9f3bde2730fc8aaa4b00419fc865f6ec573e5320234531ef37505da7da192c40b + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: ^1.0.0 + concat-map: 0.0.1 + checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: ^1.0.0 + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + +"broadcast-channel@npm:^3.4.1": + version: 3.7.0 + resolution: "broadcast-channel@npm:3.7.0" + dependencies: + "@babel/runtime": ^7.7.2 + detect-node: ^2.1.0 + js-sha3: 0.8.0 + microseconds: 0.2.0 + nano-time: 1.0.0 + oblivious-set: 1.0.0 + rimraf: 3.0.2 + unload: 2.2.0 + checksum: 803794c48dcce7f03aca69797430bd8b1c4cfd70b7de22079cd89567eeffaa126a1db98c7c2d86af8131d9bb41ed367c0fef96dfb446151c927b831572c621fc + languageName: node + linkType: hard + +"browserslist@npm:^4.22.2": + version: 4.22.3 + resolution: "browserslist@npm:4.22.3" + dependencies: + caniuse-lite: ^1.0.30001580 + electron-to-chromium: ^1.4.648 + node-releases: ^2.0.14 + update-browserslist-db: ^1.0.13 + bin: + browserslist: cli.js + checksum: e62b17348e92143fe58181b02a6a97c4a98bd812d1dc9274673a54f73eec53dbed1c855ebf73e318ee00ee039f23c9a6d0e7629d24f3baef08c7a5b469742d57 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.2 + resolution: "cacache@npm:18.0.2" + dependencies: + "@npmcli/fs": ^3.1.0 + fs-minipass: ^3.0.0 + glob: ^10.2.2 + lru-cache: ^10.0.1 + minipass: ^7.0.3 + minipass-collect: ^2.0.1 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + p-map: ^4.0.0 + ssri: ^10.0.0 + tar: ^6.1.11 + unique-filename: ^3.0.0 + checksum: 0250df80e1ad0c828c956744850c5f742c24244e9deb5b7dc81bca90f8c10e011e132ecc58b64497cc1cad9a98968676147fb6575f4f94722f7619757b17a11b + languageName: node + linkType: hard + +"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.4, call-bind@npm:^1.0.5": + version: 1.0.5 + resolution: "call-bind@npm:1.0.5" + dependencies: + function-bind: ^1.1.2 + get-intrinsic: ^1.2.1 + set-function-length: ^1.1.1 + checksum: 449e83ecbd4ba48e7eaac5af26fea3b50f8f6072202c2dd7c5a6e7a6308f2421abe5e13a3bbd55221087f76320c5e09f25a8fdad1bab2b77c68ae74d92234ea5 + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001580": + version: 1.0.30001582 + resolution: "caniuse-lite@npm:1.0.30001582" + checksum: 2fc420cb6e6080a9808781ff81a2f0d37d63897c8c981d477001be18e55c8ec33422cf966e49efdca61d4a5335d16a4d6a09d5bd6da22a8396d0dcd350b9b9da + languageName: node + linkType: hard + +"chalk@npm:^2.4.2": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: ^3.2.1 + escape-string-regexp: ^1.0.5 + supports-color: ^5.3.0 + checksum: ec3661d38fe77f681200f878edbd9448821924e0f93a9cefc0e26a33b145f1027a2084bf19967160d11e1f03bfe4eaffcabf5493b89098b2782c3fe0b03d80c2 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: 1.1.3 + checksum: fd7a64a17cde98fb923b1dd05c5f2e6f7aefda1b60d67e8d449f9328b4e53b228a428fd38bfeaeb2db2ff6b6503a776a996150b80cdf224062af08a5c8a3a203 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: ~1.1.4 + checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: ~1.0.0 + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + +"csstype@npm:^3.0.2": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: 2.1.2 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: edb65dd0d7d1b9c40b2f50219aef30e116cedd6fc79290e740972c132c09106d2e80aa0bc8826673dd5a00222d4179c84b36a790eef63a4c4bca75a37ef90804 + languageName: node + linkType: hard + +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1": + version: 1.1.1 + resolution: "define-data-property@npm:1.1.1" + dependencies: + get-intrinsic: ^1.2.1 + gopd: ^1.0.1 + has-property-descriptors: ^1.0.0 + checksum: a29855ad3f0630ea82e3c5012c812efa6ca3078d5c2aa8df06b5f597c1cde6f7254692df41945851d903e05a1668607b6d34e778f402b9ff9ffb38111f1a3f0d + languageName: node + linkType: hard + +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": + version: 1.2.1 + resolution: "define-properties@npm:1.2.1" + dependencies: + define-data-property: ^1.0.1 + has-property-descriptors: ^1.0.0 + object-keys: ^1.1.1 + checksum: b4ccd00597dd46cb2d4a379398f5b19fca84a16f3374e2249201992f36b30f6835949a9429669ee6b41b6e837205a163eadd745e472069e70dfc10f03e5fcc12 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + +"detect-node@npm:^2.0.4, detect-node@npm:^2.1.0": + version: 2.1.0 + resolution: "detect-node@npm:2.1.0" + checksum: 832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e + languageName: node + linkType: hard + +"doctrine@npm:^2.1.0": + version: 2.1.0 + resolution: "doctrine@npm:2.1.0" + dependencies: + esutils: ^2.0.2 + checksum: a45e277f7feaed309fe658ace1ff286c6e2002ac515af0aaf37145b8baa96e49899638c7cd47dccf84c3d32abfc113246625b3ac8f552d1046072adee13b0dc8 + languageName: node + linkType: hard + +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: ^2.0.2 + checksum: fd7673ca77fe26cd5cba38d816bc72d641f500f1f9b25b83e8ce28827fe2da7ad583a8da26ab6af85f834138cf8dae9f69b0cd6ab925f52ddab1754db44d99ce + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.4.648": + version: 1.4.653 + resolution: "electron-to-chromium@npm:1.4.653" + checksum: 5e1fb48e749811f4384cd7a9940585a124f46d2a9f5bdfd2d7e79685d55db448433da303bd0fb6c3665ed052299843b5661319e10119821defbc3be8ff8eb060 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: ^0.6.2 + checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 + languageName: node + linkType: hard + +"es-abstract@npm:^1.22.1": + version: 1.22.3 + resolution: "es-abstract@npm:1.22.3" + dependencies: + array-buffer-byte-length: ^1.0.0 + arraybuffer.prototype.slice: ^1.0.2 + available-typed-arrays: ^1.0.5 + call-bind: ^1.0.5 + es-set-tostringtag: ^2.0.1 + es-to-primitive: ^1.2.1 + function.prototype.name: ^1.1.6 + get-intrinsic: ^1.2.2 + get-symbol-description: ^1.0.0 + globalthis: ^1.0.3 + gopd: ^1.0.1 + has-property-descriptors: ^1.0.0 + has-proto: ^1.0.1 + has-symbols: ^1.0.3 + hasown: ^2.0.0 + internal-slot: ^1.0.5 + is-array-buffer: ^3.0.2 + is-callable: ^1.2.7 + is-negative-zero: ^2.0.2 + is-regex: ^1.1.4 + is-shared-array-buffer: ^1.0.2 + is-string: ^1.0.7 + is-typed-array: ^1.1.12 + is-weakref: ^1.0.2 + object-inspect: ^1.13.1 + object-keys: ^1.1.1 + object.assign: ^4.1.4 + regexp.prototype.flags: ^1.5.1 + safe-array-concat: ^1.0.1 + safe-regex-test: ^1.0.0 + string.prototype.trim: ^1.2.8 + string.prototype.trimend: ^1.0.7 + string.prototype.trimstart: ^1.0.7 + typed-array-buffer: ^1.0.0 + typed-array-byte-length: ^1.0.0 + typed-array-byte-offset: ^1.0.0 + typed-array-length: ^1.0.4 + unbox-primitive: ^1.0.2 + which-typed-array: ^1.1.13 + checksum: b1bdc962856836f6e72be10b58dc128282bdf33771c7a38ae90419d920fc3b36cc5d2b70a222ad8016e3fc322c367bf4e9e89fc2bc79b7e933c05b218e83d79a + languageName: node + linkType: hard + +"es-iterator-helpers@npm:^1.0.12": + version: 1.0.15 + resolution: "es-iterator-helpers@npm:1.0.15" + dependencies: + asynciterator.prototype: ^1.0.0 + call-bind: ^1.0.2 + define-properties: ^1.2.1 + es-abstract: ^1.22.1 + es-set-tostringtag: ^2.0.1 + function-bind: ^1.1.1 + get-intrinsic: ^1.2.1 + globalthis: ^1.0.3 + has-property-descriptors: ^1.0.0 + has-proto: ^1.0.1 + has-symbols: ^1.0.3 + internal-slot: ^1.0.5 + iterator.prototype: ^1.1.2 + safe-array-concat: ^1.0.1 + checksum: 50081ae5c549efe62e5c1d244df0194b40b075f7897fc2116b7e1aa437eb3c41f946d2afda18c33f9b31266ec544765932542765af839f76fa6d7b7855d1e0e1 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.0.1": + version: 2.0.2 + resolution: "es-set-tostringtag@npm:2.0.2" + dependencies: + get-intrinsic: ^1.2.2 + has-tostringtag: ^1.0.0 + hasown: ^2.0.0 + checksum: afcec3a4c9890ae14d7ec606204858441c801ff84f312538e1d1ccf1e5493c8b17bd672235df785f803756472cb4f2d49b87bde5237aef33411e74c22f194e07 + languageName: node + linkType: hard + +"es-shim-unscopables@npm:^1.0.0": + version: 1.0.2 + resolution: "es-shim-unscopables@npm:1.0.2" + dependencies: + hasown: ^2.0.0 + checksum: 432bd527c62065da09ed1d37a3f8e623c423683285e6188108286f4a1e8e164a5bcbfbc0051557c7d14633cd2a41ce24c7048e6bbb66a985413fd32f1be72626 + languageName: node + linkType: hard + +"es-to-primitive@npm:^1.2.1": + version: 1.2.1 + resolution: "es-to-primitive@npm:1.2.1" + dependencies: + is-callable: ^1.1.4 + is-date-object: ^1.0.1 + is-symbol: ^1.0.2 + checksum: 4ead6671a2c1402619bdd77f3503991232ca15e17e46222b0a41a5d81aebc8740a77822f5b3c965008e631153e9ef0580540007744521e72de8e33599fca2eed + languageName: node + linkType: hard + +"esbuild@npm:^0.19.3": + version: 0.19.12 + resolution: "esbuild@npm:0.19.12" + dependencies: + "@esbuild/aix-ppc64": 0.19.12 + "@esbuild/android-arm": 0.19.12 + "@esbuild/android-arm64": 0.19.12 + "@esbuild/android-x64": 0.19.12 + "@esbuild/darwin-arm64": 0.19.12 + "@esbuild/darwin-x64": 0.19.12 + "@esbuild/freebsd-arm64": 0.19.12 + "@esbuild/freebsd-x64": 0.19.12 + "@esbuild/linux-arm": 0.19.12 + "@esbuild/linux-arm64": 0.19.12 + "@esbuild/linux-ia32": 0.19.12 + "@esbuild/linux-loong64": 0.19.12 + "@esbuild/linux-mips64el": 0.19.12 + "@esbuild/linux-ppc64": 0.19.12 + "@esbuild/linux-riscv64": 0.19.12 + "@esbuild/linux-s390x": 0.19.12 + "@esbuild/linux-x64": 0.19.12 + "@esbuild/netbsd-x64": 0.19.12 + "@esbuild/openbsd-x64": 0.19.12 + "@esbuild/sunos-x64": 0.19.12 + "@esbuild/win32-arm64": 0.19.12 + "@esbuild/win32-ia32": 0.19.12 + "@esbuild/win32-x64": 0.19.12 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 2936e29107b43e65a775b78b7bc66ddd7d76febd73840ac7e825fb22b65029422ff51038a08d19b05154f543584bd3afe7d1ef1c63900429475b17fbe61cb61f + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.1.1 + resolution: "escalade@npm:3.1.1" + checksum: a3e2a99f07acb74b3ad4989c48ca0c3140f69f923e56d0cba0526240ee470b91010f9d39001f2a4a313841d237ede70a729e92125191ba5d21e74b106800b133 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + +"eslint-plugin-react-hooks@npm:4.6.0": + version: 4.6.0 + resolution: "eslint-plugin-react-hooks@npm:4.6.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + checksum: 23001801f14c1d16bf0a837ca7970d9dd94e7b560384b41db378b49b6e32dc43d6e2790de1bd737a652a86f81a08d6a91f402525061b47719328f586a57e86c3 + languageName: node + linkType: hard + +"eslint-plugin-react-refresh@npm:0.4.5": + version: 0.4.5 + resolution: "eslint-plugin-react-refresh@npm:0.4.5" + peerDependencies: + eslint: ">=7" + checksum: 953e665f6aaf097291a2bb07fde05466338aecd169bcd6faa708b50166912e3a757f45a45252cf7738b5e0d986224d363cc227864e98c979fe9978770b2b9f42 + languageName: node + linkType: hard + +"eslint-plugin-react@npm:7.33.2": + version: 7.33.2 + resolution: "eslint-plugin-react@npm:7.33.2" + dependencies: + array-includes: ^3.1.6 + array.prototype.flatmap: ^1.3.1 + array.prototype.tosorted: ^1.1.1 + doctrine: ^2.1.0 + es-iterator-helpers: ^1.0.12 + estraverse: ^5.3.0 + jsx-ast-utils: ^2.4.1 || ^3.0.0 + minimatch: ^3.1.2 + object.entries: ^1.1.6 + object.fromentries: ^2.0.6 + object.hasown: ^1.1.2 + object.values: ^1.1.6 + prop-types: ^15.8.1 + resolve: ^2.0.0-next.4 + semver: ^6.3.1 + string.prototype.matchall: ^4.0.8 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: b4c3d76390b0ae6b6f9fed78170604cc2c04b48e6778a637db339e8e3911ec9ef22510b0ae77c429698151d0f1b245f282177f384105b6830e7b29b9c9b26610 + languageName: node + linkType: hard + +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: ec97dbf5fb04b94e8f4c5a91a7f0a6dd3c55e46bfc7bbcd0e3138c3a76977570e02ed89a1810c778dcd72072ff0e9621ba1379b4babe53921d71e2e4486fda3e + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60 + languageName: node + linkType: hard + +"eslint@npm:8.55.0": + version: 8.55.0 + resolution: "eslint@npm:8.55.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.6.1 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.55.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 + checksum: 83f82a604559dc1faae79d28fdf3dfc9e592ca221052e2ea516e1b379b37e77e4597705a16880e2f5ece4f79087c1dd13fd7f6e9746f794a401175519db18b41 + languageName: node + linkType: hard + +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: ^8.9.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.1 + checksum: eb8c149c7a2a77b3f33a5af80c10875c3abd65450f60b8af6db1bfcfa8f101e21c1e56a561c6dc13b848e18148d43469e7cd208506238554fb5395a9ea5a1ab9 + languageName: node + linkType: hard + +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: ^5.1.0 + checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: ^5.2.0 + checksum: ebc17b1a33c51cef46fdc28b958994b1dc43cd2e86237515cbc3b4e5d2be6a811b2315d0a1a4d9d340b6d2308b15322f5c8291059521cc5f4802f65e7ec32837 + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 072780882dc8416ad144f8fe199628d2b3e7bbc9989d9ed43795d2c90309a2047e6bc5979d7e2322a341163d22cfad9e21f4110597fe487519697389497e4e2b + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 92cfec0a8dfafd9c7a15fba8f2cc29cd0b62b85f056d99ce448bbcd9f708e18ab2764bda4dd5158364f4145a7c72788538994f0d1787b956ef0d1062b0f7c24c + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.17.0 + resolution: "fastq@npm:1.17.0" + dependencies: + reusify: ^1.0.4 + checksum: a1c88c357a220bdc666c2df5ec6071d49bdf79ea827d92f9a9559da3ff1b4288eecca3ecbb7b6ddf0ba016eb0a4bf756bf17c411a6d10c814aecd26e939cbd06 + languageName: node + linkType: hard + +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: ^3.0.4 + checksum: f49701feaa6314c8127c3c2f6173cfefff17612f5ed2daaafc6da13b5c91fd43e3b2a58fd0d63f9f94478a501b167615931e7200e31485e320f74a33885a9c74 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + +"flat-cache@npm:^3.0.4": + version: 3.2.0 + resolution: "flat-cache@npm:3.2.0" + dependencies: + flatted: ^3.2.9 + keyv: ^4.5.3 + rimraf: ^3.0.2 + checksum: e7e0f59801e288b54bee5cb9681e9ee21ee28ef309f886b312c9d08415b79fc0f24ac842f84356ce80f47d6a53de62197ce0e6e148dc42d5db005992e2a756ec + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.2.9 + resolution: "flatted@npm:3.2.9" + checksum: f14167fbe26a9d20f6fca8d998e8f1f41df72c8e81f9f2c9d61ed2bea058248f5e1cbd05e7f88c0e5087a6a0b822a1e5e2b446e879f3cfbe0b07ba2d7f80b026 + languageName: node + linkType: hard + +"follow-redirects@npm:^1.15.4": + version: 1.15.5 + resolution: "follow-redirects@npm:1.15.5" + peerDependenciesMeta: + debug: + optional: true + checksum: 5ca49b5ce6f44338cbfc3546823357e7a70813cecc9b7b768158a1d32c1e62e7407c944402a918ea8c38ae2e78266312d617dc68783fac502cbb55e1047b34ec + languageName: node + linkType: hard + +"for-each@npm:^0.3.3": + version: 0.3.3 + resolution: "for-each@npm:0.3.3" + dependencies: + is-callable: ^1.1.3 + checksum: 6c48ff2bc63362319c65e2edca4a8e1e3483a2fabc72fbe7feaf8c73db94fc7861bd53bc02c8a66a0c1dd709da6b04eec42e0abdd6b40ce47305ae92a25e5d28 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + mime-types: ^2.1.12 + checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c + languageName: node + linkType: hard + +"frontent@workspace:.": + version: 0.0.0-use.local + resolution: "frontent@workspace:." + dependencies: + "@types/react": 18.2.43 + "@types/react-dom": 18.2.17 + "@vitejs/plugin-react": 4.2.1 + axios: ^1.6.2 + eslint: 8.55.0 + eslint-plugin-react: 7.33.2 + eslint-plugin-react-hooks: 4.6.0 + eslint-plugin-react-refresh: 0.4.5 + react: 18.2.0 + react-dom: 18.2.0 + react-query: 3.39.3 + react-router-dom: 6.15.0 + vite: 5.0.8 + languageName: unknown + linkType: soft + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: ^3.0.0 + checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: ^7.0.3 + checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: latest + checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@~2.3.2#~builtin, fsevents@patch:fsevents@~2.3.3#~builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.1, function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1 + languageName: node + linkType: hard + +"function.prototype.name@npm:^1.1.5, function.prototype.name@npm:^1.1.6": + version: 1.1.6 + resolution: "function.prototype.name@npm:1.1.6" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + functions-have-names: ^1.2.3 + checksum: 7a3f9bd98adab09a07f6e1f03da03d3f7c26abbdeaeee15223f6c04a9fb5674792bdf5e689dac19b97ac71de6aad2027ba3048a9b883aa1b3173eed6ab07f479 + languageName: node + linkType: hard + +"functions-have-names@npm:^1.2.3": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: c3f1f5ba20f4e962efb71344ce0a40722163e85bee2101ce25f88214e78182d2d2476aa85ef37950c579eb6cf6ee811c17b3101bb84004bb75655f3e33f3fdb5 + languageName: node + linkType: hard + +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: a7437e58c6be12aa6c90f7730eac7fa9833dc78872b4ad2963d2031b00a3367a93f98aec75f9aaac7220848e4026d67a8655e870b24f20a543d103c0d65952ec + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": + version: 1.2.2 + resolution: "get-intrinsic@npm:1.2.2" + dependencies: + function-bind: ^1.1.2 + has-proto: ^1.0.1 + has-symbols: ^1.0.3 + hasown: ^2.0.0 + checksum: 447ff0724df26829908dc033b62732359596fcf66027bc131ab37984afb33842d9cd458fd6cecadfe7eac22fd8a54b349799ed334cf2726025c921c7250e7417 + languageName: node + linkType: hard + +"get-symbol-description@npm:^1.0.0": + version: 1.0.0 + resolution: "get-symbol-description@npm:1.0.0" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.1.1 + checksum: 9ceff8fe968f9270a37a1f73bf3f1f7bda69ca80f4f80850670e0e7b9444ff99323f7ac52f96567f8b5f5fbe7ac717a0d81d3407c7313e82810c6199446a5247 + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: ^4.0.3 + checksum: c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.3.5 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + path-scurry: ^1.10.1 + bin: + glob: dist/esm/bin.mjs + checksum: 4f2fe2511e157b5a3f525a54092169a5f92405f24d2aed3142f4411df328baca13059f4182f1db1bf933e2c69c0bd89e57ae87edd8950cba8c7ccbe84f721cf3 + languageName: node + linkType: hard + +"glob@npm:^7.1.3": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + 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 + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 67051a45eca3db904aee189dfc7cd53c20c7d881679c93f6146ddd4c9f4ab2268e68a919df740d39c71f4445d2b38ee360fc234428baea1dbdfe68bbcb46979e + languageName: node + linkType: hard + +"globals@npm:^13.19.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: ^0.20.2 + checksum: 56066ef058f6867c04ff203b8a44c15b038346a62efbc3060052a1016be9f56f4cf0b2cd45b74b22b81e521a889fc7786c73691b0549c2f3a6e825b3d394f43c + languageName: node + linkType: hard + +"globalthis@npm:^1.0.3": + version: 1.0.3 + resolution: "globalthis@npm:1.0.3" + dependencies: + define-properties: ^1.1.3 + checksum: fbd7d760dc464c886d0196166d92e5ffb4c84d0730846d6621a39fbbc068aeeb9c8d1421ad330e94b7bca4bb4ea092f5f21f3d36077812af5d098b4dc006c998 + languageName: node + linkType: hard + +"gopd@npm:^1.0.1": + version: 1.0.1 + resolution: "gopd@npm:1.0.1" + dependencies: + get-intrinsic: ^1.1.3 + checksum: a5ccfb8806e0917a94e0b3de2af2ea4979c1da920bc381667c260e00e7cafdbe844e2cb9c5bcfef4e5412e8bf73bab837285bc35c7ba73aaaf0134d4583393a6 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 + languageName: node + linkType: hard + +"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": + version: 1.0.2 + resolution: "has-bigints@npm:1.0.2" + checksum: 390e31e7be7e5c6fe68b81babb73dfc35d413604d7ee5f56da101417027a4b4ce6a27e46eff97ad040c835b5d228676eae99a9b5c3bc0e23c8e81a49241ff45b + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.1": + version: 1.0.1 + resolution: "has-property-descriptors@npm:1.0.1" + dependencies: + get-intrinsic: ^1.2.2 + checksum: 2bcc6bf6ec6af375add4e4b4ef586e43674850a91ad4d46666d0b28ba8e1fd69e424c7677d24d60f69470ad0afaa2f3197f508b20b0bb7dd99a8ab77ffc4b7c4 + languageName: node + linkType: hard + +"has-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "has-proto@npm:1.0.1" + checksum: febc5b5b531de8022806ad7407935e2135f1cc9e64636c3916c6842bd7995994ca3b29871ecd7954bd35f9e2986c17b3b227880484d22259e2f8e6ce63fd383e + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": + version: 1.0.3 + resolution: "has-symbols@npm:1.0.3" + checksum: a054c40c631c0d5741a8285010a0777ea0c068f99ed43e5d6eb12972da223f8af553a455132fdb0801bdcfa0e0f443c0c03a68d8555aa529b3144b446c3f2410 + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.0": + version: 1.0.0 + resolution: "has-tostringtag@npm:1.0.0" + dependencies: + has-symbols: ^1.0.2 + checksum: cc12eb28cb6ae22369ebaad3a8ab0799ed61270991be88f208d508076a1e99abe4198c965935ce85ea90b60c94ddda73693b0920b58e7ead048b4a391b502c1c + languageName: node + linkType: hard + +"hasown@npm:^2.0.0": + version: 2.0.0 + resolution: "hasown@npm:2.0.0" + dependencies: + function-bind: ^1.1.2 + checksum: 6151c75ca12554565098641c98a40f4cc86b85b0fd5b6fe92360967e4605a4f9610f7757260b4e8098dd1c2ce7f4b095f2006fe72a570e3b6d2d28de0298c176 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" + dependencies: + agent-base: ^7.1.0 + debug: ^4.3.4 + checksum: 48d4fac997917e15f45094852b63b62a46d0c8a4f0b9c6c23ca26d27b8df8d178bed88389e604745e748bd9a01f5023e25093722777f0593c3f052009ff438b6 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.2 + resolution: "https-proxy-agent@npm:7.0.2" + dependencies: + agent-base: ^7.0.2 + debug: 4 + checksum: 088969a0dd476ea7a0ed0a2cf1283013682b08f874c3bc6696c83fa061d2c157d29ef0ad3eb70a2046010bb7665573b2388d10fdcb3e410a66995e5248444292 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: ">= 2.1.2 < 3.0.0" + checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.1 + resolution: "ignore@npm:5.3.1" + checksum: 71d7bb4c1dbe020f915fd881108cbe85a0db3d636a0ea3ba911393c53946711d13a9b1143c7e70db06d571a5822c0a324a6bcde5c9904e7ca5047f01f1bf8cd3 + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.0 + resolution: "import-fresh@npm:3.3.0" + dependencies: + parent-module: ^1.0.0 + resolve-from: ^4.0.0 + checksum: 2cacfad06e652b1edc50be650f7ec3be08c5e5a6f6d12d035c440a42a8cc028e60a5b99ca08a77ab4d6b1346da7d971915828f33cdab730d3d42f08242d09baa + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: ^1.3.0 + wrappy: 1 + checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd + languageName: node + linkType: hard + +"inherits@npm:2": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 + languageName: node + linkType: hard + +"internal-slot@npm:^1.0.5": + version: 1.0.6 + resolution: "internal-slot@npm:1.0.6" + dependencies: + get-intrinsic: ^1.2.2 + hasown: ^2.0.0 + side-channel: ^1.0.4 + checksum: 7872454888047553ce97a3fa1da7cc054a28ec5400a9c2e9f4dbe4fe7c1d041cb8e8301467614b80d4246d50377aad2fb58860b294ed74d6700cc346b6f89549 + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349 + languageName: node + linkType: hard + +"is-array-buffer@npm:^3.0.1, is-array-buffer@npm:^3.0.2": + version: 3.0.2 + resolution: "is-array-buffer@npm:3.0.2" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.2.0 + is-typed-array: ^1.1.10 + checksum: dcac9dda66ff17df9cabdc58214172bf41082f956eab30bb0d86bc0fab1e44b690fc8e1f855cf2481245caf4e8a5a006a982a71ddccec84032ed41f9d8da8c14 + languageName: node + linkType: hard + +"is-async-function@npm:^2.0.0": + version: 2.0.0 + resolution: "is-async-function@npm:2.0.0" + dependencies: + has-tostringtag: ^1.0.0 + checksum: e3471d95e6c014bf37cad8a93f2f4b6aac962178e0a5041e8903147166964fdc1c5c1d2ef87e86d77322c370ca18f2ea004fa7420581fa747bcaf7c223069dbd + languageName: node + linkType: hard + +"is-bigint@npm:^1.0.1": + version: 1.0.4 + resolution: "is-bigint@npm:1.0.4" + dependencies: + has-bigints: ^1.0.1 + checksum: c56edfe09b1154f8668e53ebe8252b6f185ee852a50f9b41e8d921cb2bed425652049fbe438723f6cb48a63ca1aa051e948e7e401e093477c99c84eba244f666 + languageName: node + linkType: hard + +"is-boolean-object@npm:^1.1.0": + version: 1.1.2 + resolution: "is-boolean-object@npm:1.1.2" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: c03b23dbaacadc18940defb12c1c0e3aaece7553ef58b162a0f6bba0c2a7e1551b59f365b91e00d2dbac0522392d576ef322628cb1d036a0fe51eb466db67222 + languageName: node + linkType: hard + +"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": + version: 1.2.7 + resolution: "is-callable@npm:1.2.7" + checksum: 61fd57d03b0d984e2ed3720fb1c7a897827ea174bd44402878e059542ea8c4aeedee0ea0985998aa5cc2736b2fa6e271c08587addb5b3959ac52cf665173d1ac + languageName: node + linkType: hard + +"is-core-module@npm:^2.13.0": + version: 2.13.1 + resolution: "is-core-module@npm:2.13.1" + dependencies: + hasown: ^2.0.0 + checksum: 256559ee8a9488af90e4bad16f5583c6d59e92f0742e9e8bb4331e758521ee86b810b93bae44f390766ffbc518a0488b18d9dab7da9a5ff997d499efc9403f7c + languageName: node + linkType: hard + +"is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5": + version: 1.0.5 + resolution: "is-date-object@npm:1.0.5" + dependencies: + has-tostringtag: ^1.0.0 + checksum: baa9077cdf15eb7b58c79398604ca57379b2fc4cf9aa7a9b9e295278648f628c9b201400c01c5e0f7afae56507d741185730307cbe7cad3b9f90a77e5ee342fc + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 + languageName: node + linkType: hard + +"is-finalizationregistry@npm:^1.0.2": + version: 1.0.2 + resolution: "is-finalizationregistry@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 4f243a8e06228cd45bdab8608d2cb7abfc20f6f0189c8ac21ea8d603f1f196eabd531ce0bb8e08cbab047e9845ef2c191a3761c9a17ad5cabf8b35499c4ad35d + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard + +"is-generator-function@npm:^1.0.10": + version: 1.0.10 + resolution: "is-generator-function@npm:1.0.10" + dependencies: + has-tostringtag: ^1.0.0 + checksum: d54644e7dbaccef15ceb1e5d91d680eb5068c9ee9f9eb0a9e04173eb5542c9b51b5ab52c5537f5703e48d5fddfd376817c1ca07a84a407b7115b769d4bdde72b + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: ^2.1.1 + checksum: d381c1319fcb69d341cc6e6c7cd588e17cd94722d9a32dbd60660b993c4fb7d0f19438674e68dfec686d09b7c73139c9166b47597f846af387450224a8101ab4 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + +"is-map@npm:^2.0.1": + version: 2.0.2 + resolution: "is-map@npm:2.0.2" + checksum: ace3d0ecd667bbdefdb1852de601268f67f2db725624b1958f279316e13fecb8fa7df91fd60f690d7417b4ec180712f5a7ee967008e27c65cfd475cc84337728 + languageName: node + linkType: hard + +"is-negative-zero@npm:^2.0.2": + version: 2.0.2 + resolution: "is-negative-zero@npm:2.0.2" + checksum: f3232194c47a549da60c3d509c9a09be442507616b69454716692e37ae9f37c4dea264fb208ad0c9f3efd15a796a46b79df07c7e53c6227c32170608b809149a + languageName: node + linkType: hard + +"is-number-object@npm:^1.0.4": + version: 1.0.7 + resolution: "is-number-object@npm:1.0.7" + dependencies: + has-tostringtag: ^1.0.0 + checksum: d1e8d01bb0a7134c74649c4e62da0c6118a0bfc6771ea3c560914d52a627873e6920dd0fd0ebc0e12ad2ff4687eac4c308f7e80320b973b2c8a2c8f97a7524f7 + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 + languageName: node + linkType: hard + +"is-regex@npm:^1.1.4": + version: 1.1.4 + resolution: "is-regex@npm:1.1.4" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: 362399b33535bc8f386d96c45c9feb04cf7f8b41c182f54174c1a45c9abbbe5e31290bbad09a458583ff6bf3b2048672cdb1881b13289569a7c548370856a652 + languageName: node + linkType: hard + +"is-set@npm:^2.0.1": + version: 2.0.2 + resolution: "is-set@npm:2.0.2" + checksum: b64343faf45e9387b97a6fd32be632ee7b269bd8183701f3b3f5b71a7cf00d04450ed8669d0bd08753e08b968beda96fca73a10fd0ff56a32603f64deba55a57 + languageName: node + linkType: hard + +"is-shared-array-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "is-shared-array-buffer@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 9508929cf14fdc1afc9d61d723c6e8d34f5e117f0bffda4d97e7a5d88c3a8681f633a74f8e3ad1fe92d5113f9b921dc5ca44356492079612f9a247efbce7032a + languageName: node + linkType: hard + +"is-string@npm:^1.0.5, is-string@npm:^1.0.7": + version: 1.0.7 + resolution: "is-string@npm:1.0.7" + dependencies: + has-tostringtag: ^1.0.0 + checksum: 323b3d04622f78d45077cf89aab783b2f49d24dc641aa89b5ad1a72114cfeff2585efc8c12ef42466dff32bde93d839ad321b26884cf75e5a7892a938b089989 + languageName: node + linkType: hard + +"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": + version: 1.0.4 + resolution: "is-symbol@npm:1.0.4" + dependencies: + has-symbols: ^1.0.2 + checksum: 92805812ef590738d9de49d677cd17dfd486794773fb6fa0032d16452af46e9b91bb43ffe82c983570f015b37136f4b53b28b8523bfb10b0ece7a66c31a54510 + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.9": + version: 1.1.12 + resolution: "is-typed-array@npm:1.1.12" + dependencies: + which-typed-array: ^1.1.11 + checksum: 4c89c4a3be07186caddadf92197b17fda663a9d259ea0d44a85f171558270d36059d1c386d34a12cba22dfade5aba497ce22778e866adc9406098c8fc4771796 + languageName: node + linkType: hard + +"is-weakmap@npm:^2.0.1": + version: 2.0.1 + resolution: "is-weakmap@npm:2.0.1" + checksum: 1222bb7e90c32bdb949226e66d26cb7bce12e1e28e3e1b40bfa6b390ba3e08192a8664a703dff2a00a84825f4e022f9cd58c4599ff9981ab72b1d69479f4f7f6 + languageName: node + linkType: hard + +"is-weakref@npm:^1.0.2": + version: 1.0.2 + resolution: "is-weakref@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 95bd9a57cdcb58c63b1c401c60a474b0f45b94719c30f548c891860f051bc2231575c290a6b420c6bc6e7ed99459d424c652bd5bf9a1d5259505dc35b4bf83de + languageName: node + linkType: hard + +"is-weakset@npm:^2.0.1": + version: 2.0.2 + resolution: "is-weakset@npm:2.0.2" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.1.1 + checksum: 5d8698d1fa599a0635d7ca85be9c26d547b317ed8fd83fc75f03efbe75d50001b5eececb1e9971de85fcde84f69ae6f8346bc92d20d55d46201d328e4c74a367 + languageName: node + linkType: hard + +"isarray@npm:^2.0.5": + version: 2.0.5 + resolution: "isarray@npm:2.0.5" + checksum: bd5bbe4104438c4196ba58a54650116007fa0262eccef13a4c55b2e09a5b36b59f1e75b9fcc49883dd9d4953892e6fc007eef9e9155648ceea036e184b0f930a + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + +"iterator.prototype@npm:^1.1.2": + version: 1.1.2 + resolution: "iterator.prototype@npm:1.1.2" + dependencies: + define-properties: ^1.2.1 + get-intrinsic: ^1.2.1 + has-symbols: ^1.0.3 + reflect.getprototypeof: ^1.0.4 + set-function-name: ^2.0.1 + checksum: d8a507e2ccdc2ce762e8a1d3f4438c5669160ac72b88b648e59a688eec6bc4e64b22338e74000518418d9e693faf2a092d2af21b9ec7dbf7763b037a54701168 + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 + languageName: node + linkType: hard + +"js-sha3@npm:0.8.0": + version: 0.8.0 + resolution: "js-sha3@npm:0.8.0" + checksum: 75df77c1fc266973f06cce8309ce010e9e9f07ec35ab12022ed29b7f0d9c8757f5a73e1b35aa24840dced0dea7059085aa143d817aea9e188e2a80d569d9adce + languageName: node + linkType: hard + +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78 + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: ^2.0.1 + bin: + js-yaml: bin/js-yaml.js + checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a + languageName: node + linkType: hard + +"jsesc@npm:^2.5.1": + version: 2.5.2 + resolution: "jsesc@npm:2.5.2" + bin: + jsesc: bin/jsesc + checksum: 4dc190771129e12023f729ce20e1e0bfceac84d73a85bc3119f7f938843fe25a4aeccb54b6494dce26fcf263d815f5f31acdefac7cc9329efb8422a4f4d9fa9d + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 7486074d3ba247769fda17d5181b345c9fb7d12e0da98b22d1d71a5db9698d8b4bd900a3ec1a4ffdd60846fc2556274a5c894d0c48795f14cb03aeae7b55260b + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: cff44156ddce9c67c44386ad5cddf91925fe06b1d217f2da9c4910d01f358c6e3989c4d5a02683c7a5667f9727ff05831f7aa8ae66c8ff691c556f0884d49215 + languageName: node + linkType: hard + +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 2a7436a93393830bce797d4626275152e37e877b265e94ca69c99e3d20c2b9dab021279146a39cdb700e71b2dd32a4cebd1514cd57cee102b1af906ce5040349 + languageName: node + linkType: hard + +"jsx-ast-utils@npm:^2.4.1 || ^3.0.0": + version: 3.3.5 + resolution: "jsx-ast-utils@npm:3.3.5" + dependencies: + array-includes: ^3.1.6 + array.prototype.flat: ^1.3.1 + object.assign: ^4.1.4 + object.values: ^1.1.6 + checksum: f4b05fa4d7b5234230c905cfa88d36dc8a58a6666975a3891429b1a8cdc8a140bca76c297225cb7a499fad25a2c052ac93934449a2c31a44fc9edd06c773780a + languageName: node + linkType: hard + +"keyv@npm:^4.5.3": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: 3.0.1 + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: ^1.2.1 + type-check: ~0.4.0 + checksum: 12c5021c859bd0f5248561bf139121f0358285ec545ebf48bb3d346820d5c61a4309535c7f387ed7d84361cf821e124ce346c6b7cef8ee09a67c1473b46d0fc4 + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: ^5.0.0 + checksum: 72eb661788a0368c099a184c59d2fee760b3831c9c1c33955e8a19ae4a21b4116e53fa736dc086cdeb9fce9f7cc508f2f92d2d3aae516f133e16a2bb59a39f5a + languageName: node + linkType: hard + +"lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 + languageName: node + linkType: hard + +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: ^3.0.0 || ^4.0.0 + bin: + loose-envify: cli.js + checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: eee7ddda4a7475deac51ac81d7dd78709095c6fa46e8350dc2d22462559a1faa3b81ed931d5464b13d48cbd7e08b46100b6f768c76833912bc444b99c37e25db + languageName: node + linkType: hard + +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: ^3.0.2 + checksum: c154ae1cbb0c2206d1501a0e94df349653c92c8cbb25236d7e85190bcaf4567a03ac6eb43166fabfa36fd35623694da7233e88d9601fbf411a9a481d85dbd2cb + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: ^4.0.0 + checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" + dependencies: + "@npmcli/agent": ^2.0.0 + cacache: ^18.0.0 + http-cache-semantics: ^4.1.1 + is-lambda: ^1.0.1 + minipass: ^7.0.2 + minipass-fetch: ^3.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + promise-retry: ^2.0.1 + ssri: ^10.0.0 + checksum: 7c7a6d381ce919dd83af398b66459a10e2fe8f4504f340d1d090d3fa3d1b0c93750220e1d898114c64467223504bd258612ba83efbc16f31b075cd56de24b4af + languageName: node + linkType: hard + +"match-sorter@npm:^6.0.2": + version: 6.3.3 + resolution: "match-sorter@npm:6.3.3" + dependencies: + "@babel/runtime": ^7.23.8 + remove-accents: 0.5.0 + checksum: 8191bc5a39a33ec62571a171dff24ce3f604f4fcb0667c4bd512ec39834f4af6db759f91abdba3f91cf3e0ee9229f6e3d8953ec6093efb05b5f718509c9c3618 + languageName: node + linkType: hard + +"microseconds@npm:0.2.0": + version: 0.2.0 + resolution: "microseconds@npm:0.2.0" + checksum: 22bfa8553f92c7d95afff6de0aeb2aecf750680d41b8c72b02098ccc5bbbb0a384380ff539292dbd3788f5dfc298682f9d38a2b4c101f5ee2c9471d53934c5fa + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: 1.52.0 + checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 + languageName: node + linkType: hard + +"minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: ^1.1.7 + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: ^7.0.3 + checksum: b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: ^0.1.13 + minipass: ^7.0.3 + minipass-sized: ^1.0.3 + minizlib: ^2.1.2 + dependenciesMeta: + encoding: + optional: true + checksum: af7aad15d5c128ab1ebe52e043bdf7d62c3c6f0cecb9285b40d7b395e1375b45dcdfd40e63e93d26a0e8249c9efd5c325c65575aceee192883970ff8cb11364a + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: ^3.0.0 + checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: ^3.0.0 + checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: ^3.0.0 + checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: ^4.0.0 + checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: ^3.0.0 + yallist: ^4.0.0 + checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + +"nano-time@npm:1.0.0": + version: 1.0.0 + resolution: "nano-time@npm:1.0.0" + dependencies: + big-integer: ^1.6.16 + checksum: eef8548546cc1020625f8e44751a7263e9eddf0412a6a1a6c80a8d2be2ea7973622804a977cdfe796807b85b20ff6c8ba340e8dd20effcc7078193ed5edbb5d4 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 23ad088b08f898fc9b53011d7bb78ec48e79de7627e01ab5518e806033861bef68d5b0cd0e2205c2f36690ac9571ff6bcb05eb777ced2eeda8d4ac5b44592c3d + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.0.1 + resolution: "node-gyp@npm:10.0.1" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + glob: ^10.3.10 + graceful-fs: ^4.2.6 + make-fetch-happen: ^13.0.0 + nopt: ^7.0.0 + proc-log: ^3.0.0 + semver: ^7.3.5 + tar: ^6.1.2 + which: ^4.0.0 + bin: + node-gyp: bin/node-gyp.js + checksum: 60a74e66d364903ce02049966303a57f898521d139860ac82744a5fdd9f7b7b3b61f75f284f3bfe6e6add3b8f1871ce305a1d41f775c7482de837b50c792223f + languageName: node + linkType: hard + +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 59443a2f77acac854c42d321bf1b43dea0aef55cd544c6a686e9816a697300458d4e82239e2d794ea05f7bbbc8a94500332e2d3ac3f11f52e4b16cbe638b3c41 + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" + dependencies: + abbrev: ^2.0.0 + bin: + nopt: bin/nopt.js + checksum: a9c0f57fb8cb9cc82ae47192ca2b7ef00e199b9480eed202482c962d61b59a7fbe7541920b2a5839a97b42ee39e288c0aed770e38057a608d7f579389dfde410 + languageName: node + linkType: hard + +"object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.1, object-inspect@npm:^1.9.0": + version: 1.13.1 + resolution: "object-inspect@npm:1.13.1" + checksum: 7d9fa9221de3311dcb5c7c307ee5dc011cdd31dc43624b7c184b3840514e118e05ef0002be5388304c416c0eb592feb46e983db12577fc47e47d5752fbbfb61f + languageName: node + linkType: hard + +"object-keys@npm:^1.1.1": + version: 1.1.1 + resolution: "object-keys@npm:1.1.1" + checksum: b363c5e7644b1e1b04aa507e88dcb8e3a2f52b6ffd0ea801e4c7a62d5aa559affe21c55a07fd4b1fd55fc03a33c610d73426664b20032405d7b92a1414c34d6a + languageName: node + linkType: hard + +"object.assign@npm:^4.1.4": + version: 4.1.5 + resolution: "object.assign@npm:4.1.5" + dependencies: + call-bind: ^1.0.5 + define-properties: ^1.2.1 + has-symbols: ^1.0.3 + object-keys: ^1.1.1 + checksum: f9aeac0541661370a1fc86e6a8065eb1668d3e771f7dbb33ee54578201336c057b21ee61207a186dd42db0c62201d91aac703d20d12a79fc79c353eed44d4e25 + languageName: node + linkType: hard + +"object.entries@npm:^1.1.6": + version: 1.1.7 + resolution: "object.entries@npm:1.1.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: da287d434e7e32989586cd734382364ba826a2527f2bc82e6acbf9f9bfafa35d51018b66ec02543ffdfa2a5ba4af2b6f1ca6e588c65030cb4fd9c67d6ced594c + languageName: node + linkType: hard + +"object.fromentries@npm:^2.0.6": + version: 2.0.7 + resolution: "object.fromentries@npm:2.0.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: 7341ce246e248b39a431b87a9ddd331ff52a454deb79afebc95609f94b1f8238966cf21f52188f2a353f0fdf83294f32f1ebf1f7826aae915ebad21fd0678065 + languageName: node + linkType: hard + +"object.hasown@npm:^1.1.2": + version: 1.1.3 + resolution: "object.hasown@npm:1.1.3" + dependencies: + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: 76bc17356f6124542fb47e5d0e78d531eafa4bba3fc2d6fc4b1a8ce8b6878912366c0d99f37ce5c84ada8fd79df7aa6ea1214fddf721f43e093ad2df51f27da1 + languageName: node + linkType: hard + +"object.values@npm:^1.1.6": + version: 1.1.7 + resolution: "object.values@npm:1.1.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: f3e4ae4f21eb1cc7cebb6ce036d4c67b36e1c750428d7b7623c56a0db90edced63d08af8a316d81dfb7c41a3a5fa81b05b7cc9426e98d7da986b1682460f0777 + languageName: node + linkType: hard + +"oblivious-set@npm:1.0.0": + version: 1.0.0 + resolution: "oblivious-set@npm:1.0.0" + checksum: f31740ea9c3a8242ad2324e4ebb9a35359fbc2e6e7131731a0fc1c8b7b1238eb07e4c8c631a38535243a7b8e3042b7e89f7dc2a95d2989afd6f80bd5793b0aab + languageName: node + linkType: hard + +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: 1 + checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.3 + resolution: "optionator@npm:0.9.3" + 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 + checksum: 09281999441f2fe9c33a5eeab76700795365a061563d66b098923eb719251a42bdbe432790d35064d0816ead9296dbeb1ad51a733edf4167c96bd5d0882e428a + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: ^0.1.0 + checksum: 7c3690c4dbf62ef625671e20b7bdf1cbc9534e83352a2780f165b0d3ceba21907e77ad63401708145ca4e25bfc51636588d89a8c0aeb715e6c37d1c066430360 + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: ^3.0.2 + checksum: 1623088f36cf1cbca58e9b61c4e62bf0c60a07af5ae1ca99a720837356b5b6c5ba3eb1b2127e47a06865fee59dd0453cad7cc844cda9d5a62ac1a5a51b7c86d3 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: ^3.0.0 + checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: ^3.0.0 + checksum: 6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 505807199dfb7c50737b057dd8d351b82c033029ab94cb10a657609e00c1bc53b951cfdbccab8de04c5584d5eff31128ce6afd3db79281874a5ef2adbba55ed1 + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: ^9.1.1 || ^10.0.0 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 + languageName: node + linkType: hard + +"picocolors@npm:^1.0.0": + version: 1.0.0 + resolution: "picocolors@npm:1.0.0" + checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 + languageName: node + linkType: hard + +"postcss@npm:^8.4.32": + version: 8.4.33 + resolution: "postcss@npm:8.4.33" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.0.0 + source-map-js: ^1.0.2 + checksum: 6f98b2af4b76632a3de20c4f47bf0e984a1ce1a531cf11adcb0b1d63a6cbda0aae4165e578b66c32ca4879038e3eaad386a6be725a8fb4429c78e3c1ab858fe9 + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: cd192ec0d0a8e4c6da3bb80e4f62afe336df3f76271ac6deb0e6a36187133b6073a19e9727a1ff108cd8b9982e4768850d413baa71214dd80c7979617dca827a + languageName: node + linkType: hard + +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: 02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: ^2.0.2 + retry: ^0.12.0 + checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 + languageName: node + linkType: hard + +"prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: ^1.4.0 + object-assign: ^4.1.1 + react-is: ^16.13.1 + checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: b676f8c040cdc5b12723ad2f91414d267605b26419d5c821ff03befa817ddd10e238d22b25d604920340fd73efd8ba795465a0377c4adf45a4a41e4234e42dc4 + languageName: node + linkType: hard + +"react-dom@npm:18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc + languageName: node + linkType: hard + +"react-is@npm:^16.13.1": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f + languageName: node + linkType: hard + +"react-query@npm:3.39.3": + version: 3.39.3 + resolution: "react-query@npm:3.39.3" + dependencies: + "@babel/runtime": ^7.5.5 + broadcast-channel: ^3.4.1 + match-sorter: ^6.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: d2de6a0992dbf039ff2de564de1ae6361f8ac7310159dae42ec16f833b79c05caedced187235c42373ac331cc5f2fe9e2b31b14ae75a815e86d86e30ca9887ad + languageName: node + linkType: hard + +"react-refresh@npm:^0.14.0": + version: 0.14.0 + resolution: "react-refresh@npm:0.14.0" + checksum: dc69fa8c993df512f42dd0f1b604978ae89bd747c0ed5ec595c0cc50d535fb2696619ccd98ae28775cc01d0a7c146a532f0f7fb81dc22e1977c242a4912312f4 + languageName: node + linkType: hard + +"react-router-dom@npm:6.15.0": + version: 6.15.0 + resolution: "react-router-dom@npm:6.15.0" + dependencies: + "@remix-run/router": 1.8.0 + react-router: 6.15.0 + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 95301837e293654f00934de6a4bdb27bfb06f613503e4cce7a93f19384793729832e7479d50faf3b9457d149014d4df40a3ee3a5193d7e3a3caadb7aaa6ec0f9 + languageName: node + linkType: hard + +"react-router@npm:6.15.0": + version: 6.15.0 + resolution: "react-router@npm:6.15.0" + dependencies: + "@remix-run/router": 1.8.0 + peerDependencies: + react: ">=16.8" + checksum: 345b29277e13997f2625f0037f537eaf1955bb9f44ebfea80dd3ff83fc06273f7b64e1be944bfc75945fd2af5af917874133a8a93ed5ecaca523be8f045ae166 + languageName: node + linkType: hard + +"react@npm:18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b + languageName: node + linkType: hard + +"reflect.getprototypeof@npm:^1.0.4": + version: 1.0.4 + resolution: "reflect.getprototypeof@npm:1.0.4" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + get-intrinsic: ^1.2.1 + globalthis: ^1.0.3 + which-builtin-type: ^1.1.3 + checksum: 16e2361988dbdd23274b53fb2b1b9cefeab876c3941a2543b4cadac6f989e3db3957b07a44aac46cfceb3e06e2871785ec2aac992d824f76292f3b5ee87f66f2 + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 9f57c93277b5585d3c83b0cf76be47b473ae8c6d9142a46ce8b0291a04bb2cf902059f0f8445dcabb3fb7378e5fe4bb4ea1e008876343d42e46d3b484534ce38 + languageName: node + linkType: hard + +"regexp.prototype.flags@npm:^1.5.0, regexp.prototype.flags@npm:^1.5.1": + version: 1.5.1 + resolution: "regexp.prototype.flags@npm:1.5.1" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + set-function-name: ^2.0.0 + checksum: 869edff00288442f8d7fa4c9327f91d85f3b3acf8cbbef9ea7a220345cf23e9241b6def9263d2c1ebcf3a316b0aa52ad26a43a84aa02baca3381717b3e307f47 + languageName: node + linkType: hard + +"remove-accents@npm:0.5.0": + version: 0.5.0 + resolution: "remove-accents@npm:0.5.0" + checksum: 7045b37015acb03df406d21f9cbe93c3fcf2034189f5d2e33b1dace9c7d6bdcd839929905ced21a5d76c58553557e1a42651930728702312a5774179d5b9147b + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: f4ba0b8494846a5066328ad33ef8ac173801a51739eb4d63408c847da9a2e1c1de1e6cbbf72699211f3d13f8fc1325648b169bd15eb7da35688e30a5fb0e4a7f + languageName: node + linkType: hard + +"resolve@npm:^2.0.0-next.4": + version: 2.0.0-next.5 + resolution: "resolve@npm:2.0.0-next.5" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: a73ac69a1c4bd34c56b213d91f5b17ce390688fdb4a1a96ed3025cc7e08e7bfb90b3a06fcce461780cb0b589c958afcb0080ab802c71c01a7ecc8c64feafc89f + languageName: node + linkType: hard + +"resolve@patch:resolve@^2.0.0-next.4#~builtin": + version: 2.0.0-next.5 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#~builtin::version=2.0.0-next.5&hash=c3c19d" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 064d09c1808d0c51b3d90b5d27e198e6d0c5dad0eb57065fd40803d6a20553e5398b07f76739d69cbabc12547058bec6b32106ea66622375fb0d7e8fca6a846c + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: c3076ebcc22a6bc252cb0b9c77561795256c22b757f40c0d8110b1300723f15ec0fc8685e8d4ea6d7666f36c79ccc793b1939c748bf36f18f542744a4e379fcc + languageName: node + linkType: hard + +"rimraf@npm:3.0.2, rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: ^7.1.3 + bin: + rimraf: bin.js + checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 + languageName: node + linkType: hard + +"rollup@npm:^4.2.0": + version: 4.9.6 + resolution: "rollup@npm:4.9.6" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.9.6 + "@rollup/rollup-android-arm64": 4.9.6 + "@rollup/rollup-darwin-arm64": 4.9.6 + "@rollup/rollup-darwin-x64": 4.9.6 + "@rollup/rollup-linux-arm-gnueabihf": 4.9.6 + "@rollup/rollup-linux-arm64-gnu": 4.9.6 + "@rollup/rollup-linux-arm64-musl": 4.9.6 + "@rollup/rollup-linux-riscv64-gnu": 4.9.6 + "@rollup/rollup-linux-x64-gnu": 4.9.6 + "@rollup/rollup-linux-x64-musl": 4.9.6 + "@rollup/rollup-win32-arm64-msvc": 4.9.6 + "@rollup/rollup-win32-ia32-msvc": 4.9.6 + "@rollup/rollup-win32-x64-msvc": 4.9.6 + "@types/estree": 1.0.5 + fsevents: ~2.3.2 + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: cdc0bdd41ee2d3fe7f01df26f5a85921caf46ffe0ae118b2f3deebdf569e8b1c1800b8eee04960425e67aecbd9ccdd37bcdb92595866adb3968d223a07e9b7e6 + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: ^1.2.2 + checksum: cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d + languageName: node + linkType: hard + +"safe-array-concat@npm:^1.0.1": + version: 1.1.0 + resolution: "safe-array-concat@npm:1.1.0" + dependencies: + call-bind: ^1.0.5 + get-intrinsic: ^1.2.2 + has-symbols: ^1.0.3 + isarray: ^2.0.5 + checksum: 5c71eaa999168ee7474929f1cd3aae80f486353a651a094d9968936692cf90aa065224929a6486dcda66334a27dce4250a83612f9e0fef6dced1a925d3ac7296 + languageName: node + linkType: hard + +"safe-regex-test@npm:^1.0.0": + version: 1.0.2 + resolution: "safe-regex-test@npm:1.0.2" + dependencies: + call-bind: ^1.0.5 + get-intrinsic: ^1.2.2 + is-regex: ^1.1.4 + checksum: 4af5ce05a2daa4f6d4bfd5a3c64fc33d6b886f6592122e93c0efad52f7147b9b605e5ffc03c269a1e3d1f8db2a23bc636628a961c9fd65bafdc09503330673fd + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 + languageName: node + linkType: hard + +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: ^1.1.0 + checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a + languageName: node + linkType: hard + +"semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + languageName: node + linkType: hard + +"semver@npm:^7.3.5": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + +"set-function-length@npm:^1.1.1": + version: 1.2.0 + resolution: "set-function-length@npm:1.2.0" + dependencies: + define-data-property: ^1.1.1 + function-bind: ^1.1.2 + get-intrinsic: ^1.2.2 + gopd: ^1.0.1 + has-property-descriptors: ^1.0.1 + checksum: 63e34b45a2ff9abb419f52583481bf8ba597d33c0c85e56999085eb6078a0f7fbb4222051981c287feceeb358aa7789e7803cea2c82ac94c0ab37059596aff79 + languageName: node + linkType: hard + +"set-function-name@npm:^2.0.0, set-function-name@npm:^2.0.1": + version: 2.0.1 + resolution: "set-function-name@npm:2.0.1" + dependencies: + define-data-property: ^1.0.1 + functions-have-names: ^1.2.3 + has-property-descriptors: ^1.0.0 + checksum: 4975d17d90c40168eee2c7c9c59d023429f0a1690a89d75656306481ece0c3c1fb1ebcc0150ea546d1913e35fbd037bace91372c69e543e51fc5d1f31a9fa126 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: ^3.0.0 + checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"side-channel@npm:^1.0.4": + version: 1.0.4 + resolution: "side-channel@npm:1.0.4" + dependencies: + call-bind: ^1.0.0 + get-intrinsic: ^1.0.2 + object-inspect: ^1.9.0 + checksum: 351e41b947079c10bd0858364f32bb3a7379514c399edb64ab3dce683933483fc63fb5e4efe0a15a2e8a7e3c436b6a91736ddb8d8c6591b0460a24bb4a1ee245 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.1": + version: 8.0.2 + resolution: "socks-proxy-agent@npm:8.0.2" + dependencies: + agent-base: ^7.0.2 + debug: ^4.3.4 + socks: ^2.7.1 + checksum: 4fb165df08f1f380881dcd887b3cdfdc1aba3797c76c1e9f51d29048be6e494c5b06d68e7aea2e23df4572428f27a3ec22b3d7c75c570c5346507433899a4b6d + languageName: node + linkType: hard + +"socks@npm:^2.7.1": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: ^2.0.0 + smart-buffer: ^4.2.0 + checksum: 259d9e3e8e1c9809a7f5c32238c3d4d2a36b39b83851d0f573bfde5f21c4b1288417ce1af06af1452569cd1eb0841169afd4998f0e04ba04656f6b7f0e46d748 + languageName: node + linkType: hard + +"source-map-js@npm:^1.0.2": + version: 1.0.2 + resolution: "source-map-js@npm:1.0.2" + checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: ^7.0.3 + checksum: 0a31b65f21872dea1ed3f7c200d7bc1c1b91c15e419deca14f282508ba917cbb342c08a6814c7f68ca4ca4116dd1a85da2bbf39227480e50125a1ceffeecb750 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: ^8.0.0 + is-fullwidth-code-point: ^3.0.0 + strip-ansi: ^6.0.1 + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + +"string.prototype.matchall@npm:^4.0.8": + version: 4.0.10 + resolution: "string.prototype.matchall@npm:4.0.10" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + get-intrinsic: ^1.2.1 + has-symbols: ^1.0.3 + internal-slot: ^1.0.5 + regexp.prototype.flags: ^1.5.0 + set-function-name: ^2.0.0 + side-channel: ^1.0.4 + checksum: 3c78bdeff39360c8e435d7c4c6ea19f454aa7a63eda95fa6fadc3a5b984446a2f9f2c02d5c94171ce22268a573524263fbd0c8edbe3ce2e9890d7cc036cdc3ed + languageName: node + linkType: hard + +"string.prototype.trim@npm:^1.2.8": + version: 1.2.8 + resolution: "string.prototype.trim@npm:1.2.8" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: 49eb1a862a53aba73c3fb6c2a53f5463173cb1f4512374b623bcd6b43ad49dd559a06fb5789bdec771a40fc4d2a564411c0a75d35fb27e76bbe738c211ecff07 + languageName: node + linkType: hard + +"string.prototype.trimend@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimend@npm:1.0.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: 2375516272fd1ba75992f4c4aa88a7b5f3c7a9ca308d963bcd5645adf689eba6f8a04ebab80c33e30ec0aefc6554181a3a8416015c38da0aa118e60ec896310c + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimstart@npm:1.0.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + checksum: 13d0c2cb0d5ff9e926fa0bec559158b062eed2b68cd5be777ffba782c96b2b492944e47057274e064549b94dd27cf81f48b27a31fee8af5b574cff253e7eb613 + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: ^5.0.1 + checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: ^6.0.1 + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: ^3.0.0 + checksum: 95f6f4ba5afdf92f495b5a912d4abee8dcba766ae719b975c56c084f5004845f6f5a5f7769f52d53f40e21952a6d87411bafe34af4a01e65f9926002e38e1dac + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: ^4.0.0 + checksum: 3dda818de06ebbe5b9653e07842d9479f3555ebc77e9a0280caf5a14fb877ffee9ed57007c3b78f5a6324b8dbeec648d9e97a24e2ed9fdb81ddc69ea07100f4a + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.2.0 + resolution: "tar@npm:6.2.0" + dependencies: + chownr: ^2.0.0 + fs-minipass: ^2.0.0 + minipass: ^5.0.0 + minizlib: ^2.1.1 + mkdirp: ^1.0.3 + yallist: ^4.0.0 + checksum: db4d9fe74a2082c3a5016630092c54c8375ff3b280186938cfd104f2e089c4fd9bad58688ef6be9cf186a889671bf355c7cda38f09bbf60604b281715ca57f5c + languageName: node + linkType: hard + +"text-table@npm:^0.2.0": + version: 0.2.0 + resolution: "text-table@npm:0.2.0" + checksum: b6937a38c80c7f84d9c11dd75e49d5c44f71d95e810a3250bd1f1797fc7117c57698204adf676b71497acc205d769d65c16ae8fa10afad832ae1322630aef10a + languageName: node + linkType: hard + +"to-fast-properties@npm:^2.0.0": + version: 2.0.0 + resolution: "to-fast-properties@npm:2.0.0" + checksum: be2de62fe58ead94e3e592680052683b1ec986c72d589e7b21e5697f8744cdbf48c266fa72f6c15932894c10187b5f54573a3bcf7da0bfd964d5caf23d436168 + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: ^1.2.1 + checksum: ec688ebfc9c45d0c30412e41ca9c0cdbd704580eb3a9ccf07b9b576094d7b86a012baebc95681999dd38f4f444afd28504cb3a89f2ef16b31d4ab61a0739025a + languageName: node + linkType: hard + +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 4fb3272df21ad1c552486f8a2f8e115c09a521ad7a8db3d56d53718d0c907b62c6e9141ba5f584af3f6830d0872c521357e512381f24f7c44acae583ad517d73 + languageName: node + linkType: hard + +"typed-array-buffer@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-buffer@npm:1.0.0" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.2.1 + is-typed-array: ^1.1.10 + checksum: 3e0281c79b2a40cd97fe715db803884301993f4e8c18e8d79d75fd18f796e8cd203310fec8c7fdb5e6c09bedf0af4f6ab8b75eb3d3a85da69328f28a80456bd3 + languageName: node + linkType: hard + +"typed-array-byte-length@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-byte-length@npm:1.0.0" + dependencies: + call-bind: ^1.0.2 + for-each: ^0.3.3 + has-proto: ^1.0.1 + is-typed-array: ^1.1.10 + checksum: b03db16458322b263d87a702ff25388293f1356326c8a678d7515767ef563ef80e1e67ce648b821ec13178dd628eb2afdc19f97001ceae7a31acf674c849af94 + languageName: node + linkType: hard + +"typed-array-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "typed-array-byte-offset@npm:1.0.0" + dependencies: + available-typed-arrays: ^1.0.5 + call-bind: ^1.0.2 + for-each: ^0.3.3 + has-proto: ^1.0.1 + is-typed-array: ^1.1.10 + checksum: 04f6f02d0e9a948a95fbfe0d5a70b002191fae0b8fe0fe3130a9b2336f043daf7a3dda56a31333c35a067a97e13f539949ab261ca0f3692c41603a46a94e960b + languageName: node + linkType: hard + +"typed-array-length@npm:^1.0.4": + version: 1.0.4 + resolution: "typed-array-length@npm:1.0.4" + dependencies: + call-bind: ^1.0.2 + for-each: ^0.3.3 + is-typed-array: ^1.1.9 + checksum: 2228febc93c7feff142b8c96a58d4a0d7623ecde6c7a24b2b98eb3170e99f7c7eff8c114f9b283085cd59dcd2bd43aadf20e25bba4b034a53c5bb292f71f8956 + languageName: node + linkType: hard + +"unbox-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "unbox-primitive@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + has-bigints: ^1.0.2 + has-symbols: ^1.0.3 + which-boxed-primitive: ^1.0.2 + checksum: b7a1cf5862b5e4b5deb091672ffa579aa274f648410009c81cca63fed3b62b610c4f3b773f912ce545bb4e31edc3138975b5bc777fc6e4817dca51affb6380e9 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: ^4.0.0 + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 + languageName: node + linkType: hard + +"unload@npm:2.2.0": + version: 2.2.0 + resolution: "unload@npm:2.2.0" + dependencies: + "@babel/runtime": ^7.6.2 + detect-node: ^2.0.4 + checksum: 88ba950c5ff83ab4f9bbd8f63bbf19ba09687ed3c434efd43b7338cc595bc574df8f9b155ee6eee7a435de3d3a4a226726988428977a68ba4907045f1fac5d41 + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.0.13": + version: 1.0.13 + resolution: "update-browserslist-db@npm:1.0.13" + dependencies: + escalade: ^3.1.1 + picocolors: ^1.0.0 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 1e47d80182ab6e4ad35396ad8b61008ae2a1330221175d0abd37689658bdb61af9b705bfc41057fd16682474d79944fb2d86767c5ed5ae34b6276b9bed353322 + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: ^2.1.0 + checksum: 7167432de6817fe8e9e0c9684f1d2de2bb688c94388f7569f7dbdb1587c9f4ca2a77962f134ec90be0cc4d004c939ff0d05acc9f34a0db39a3c797dada262633 + languageName: node + linkType: hard + +"vite@npm:5.0.8": + version: 5.0.8 + resolution: "vite@npm:5.0.8" + dependencies: + esbuild: ^0.19.3 + fsevents: ~2.3.3 + postcss: ^8.4.32 + rollup: ^4.2.0 + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 884dc2f17aa2c41786c5acd4ff420d1f291fef7653786ed7bd59d4275e18483c94eabe44dd3a9741798ef65270dcbb6edfa280b60004314db50ac43476ff3d81 + languageName: node + linkType: hard + +"which-boxed-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "which-boxed-primitive@npm:1.0.2" + dependencies: + is-bigint: ^1.0.1 + is-boolean-object: ^1.1.0 + is-number-object: ^1.0.4 + is-string: ^1.0.5 + is-symbol: ^1.0.3 + checksum: 53ce774c7379071729533922adcca47220228405e1895f26673bbd71bdf7fb09bee38c1d6399395927c6289476b5ae0629863427fd151491b71c4b6cb04f3a5e + languageName: node + linkType: hard + +"which-builtin-type@npm:^1.1.3": + version: 1.1.3 + resolution: "which-builtin-type@npm:1.1.3" + dependencies: + function.prototype.name: ^1.1.5 + has-tostringtag: ^1.0.0 + is-async-function: ^2.0.0 + is-date-object: ^1.0.5 + is-finalizationregistry: ^1.0.2 + is-generator-function: ^1.0.10 + is-regex: ^1.1.4 + is-weakref: ^1.0.2 + isarray: ^2.0.5 + which-boxed-primitive: ^1.0.2 + which-collection: ^1.0.1 + which-typed-array: ^1.1.9 + checksum: 43730f7d8660ff9e33d1d3f9f9451c4784265ee7bf222babc35e61674a11a08e1c2925019d6c03154fcaaca4541df43abe35d2720843b9b4cbcebdcc31408f36 + languageName: node + linkType: hard + +"which-collection@npm:^1.0.1": + version: 1.0.1 + resolution: "which-collection@npm:1.0.1" + dependencies: + is-map: ^2.0.1 + is-set: ^2.0.1 + is-weakmap: ^2.0.1 + is-weakset: ^2.0.1 + checksum: c815bbd163107ef9cb84f135e6f34453eaf4cca994e7ba85ddb0d27cea724c623fae2a473ceccfd5549c53cc65a5d82692de418166df3f858e1e5dc60818581c + languageName: node + linkType: hard + +"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.9": + version: 1.1.13 + resolution: "which-typed-array@npm:1.1.13" + dependencies: + available-typed-arrays: ^1.0.5 + call-bind: ^1.0.4 + for-each: ^0.3.3 + gopd: ^1.0.1 + has-tostringtag: ^1.0.0 + checksum: 3828a0d5d72c800e369d447e54c7620742a4cc0c9baf1b5e8c17e9b6ff90d8d861a3a6dd4800f1953dbf80e5e5cec954a289e5b4a223e3bee4aeb1f8c5f33309 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: ^2.0.0 + bin: + node-which: ./bin/node-which + checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: ^3.1.1 + bin: + node-which: bin/which.js + checksum: f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 48f7bb00dc19fc635a13a39fe547f527b10c9290e7b3e836b9a8f1ca04d4d342e85714416b3c2ab74949c9c66f9cebb0473e6bc353b79035356103b47641285d + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 + languageName: node + linkType: hard diff --git a/examples/loco_example/migration/Cargo.toml b/examples/loco_example/migration/Cargo.toml new file mode 100644 index 0000000000..3729f89a98 --- /dev/null +++ b/examples/loco_example/migration/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +loco-rs = { version = "0.16" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dependencies.sea-orm-migration] +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature +] +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/actix3_example/migration/README.md b/examples/loco_example/migration/README.md similarity index 88% rename from examples/actix3_example/migration/README.md rename to examples/loco_example/migration/README.md index 963caaeb62..b3ea53eb44 100644 --- a/examples/actix3_example/migration/README.md +++ b/examples/loco_example/migration/README.md @@ -1,5 +1,9 @@ # Running Migrator CLI +- Generate a new migration file + ```sh + cargo run -- migrate generate MIGRATION_NAME + ``` - Apply all pending migrations ```sh cargo run diff --git a/examples/actix3_example/migration/src/lib.rs b/examples/loco_example/migration/src/lib.rs similarity index 54% rename from examples/actix3_example/migration/src/lib.rs rename to examples/loco_example/migration/src/lib.rs index af8d9b2ac2..f2a0111c35 100644 --- a/examples/actix3_example/migration/src/lib.rs +++ b/examples/loco_example/migration/src/lib.rs @@ -1,12 +1,14 @@ +#![allow(elided_lifetimes_in_paths)] +#![allow(clippy::wildcard_imports)] pub use sea_orm_migration::prelude::*; -mod m20220120_000001_create_post_table; +mod m20231103_114510_notes; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![Box::new(m20231103_114510_notes::Migration)] } } diff --git a/examples/loco_example/migration/src/m20231103_114510_notes.rs b/examples/loco_example/migration/src/m20231103_114510_notes.rs new file mode 100644 index 0000000000..04f13f4867 --- /dev/null +++ b/examples/loco_example/migration/src/m20231103_114510_notes.rs @@ -0,0 +1,25 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("notes") + .col(pk_auto("id")) + .col(string_null("title")) + .col(string_null("content")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("notes").to_owned()) + .await + } +} diff --git a/examples/actix3_example/migration/src/main.rs b/examples/loco_example/migration/src/main.rs similarity index 84% rename from examples/actix3_example/migration/src/main.rs rename to examples/loco_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/actix3_example/migration/src/main.rs +++ b/examples/loco_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/loco_example/src/app.rs b/examples/loco_example/src/app.rs new file mode 100644 index 0000000000..7270c102fd --- /dev/null +++ b/examples/loco_example/src/app.rs @@ -0,0 +1,57 @@ +use std::path::Path; + +use async_trait::async_trait; +use loco_rs::{ + Result, + app::{AppContext, Hooks}, + bgworker::Queue, + boot::{BootResult, StartMode, create_app}, + config::Config, + controller::AppRoutes, + db::{self, truncate_table}, + environment::Environment, + task::Tasks, +}; +use migration::Migrator; + +use crate::{controllers, models::_entities::notes}; + +pub struct App; +#[async_trait] +impl Hooks for App { + fn app_name() -> &'static str { + env!("CARGO_CRATE_NAME") + } + + async fn boot( + mode: StartMode, + environment: &Environment, + config: Config, + ) -> Result { + create_app::(mode, environment, config).await + } + + fn routes(_ctx: &AppContext) -> AppRoutes { + AppRoutes::with_default_routes() + .prefix("/api") + .add_route(controllers::notes::routes()) + } + + async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> { + Ok(()) + } + + fn register_tasks(_tasks: &mut Tasks) {} + + async fn truncate(ctx: &AppContext) -> Result<()> { + let db = &ctx.db; + truncate_table(db, notes::Entity).await?; + Ok(()) + } + + async fn seed(ctx: &AppContext, base: &Path) -> Result<()> { + let db = &ctx.db; + db::seed::(db, &base.join("notes.yaml").display().to_string()).await?; + Ok(()) + } +} diff --git a/examples/loco_example/src/bin/main.rs b/examples/loco_example/src/bin/main.rs new file mode 100644 index 0000000000..07b1e7d72d --- /dev/null +++ b/examples/loco_example/src/bin/main.rs @@ -0,0 +1,9 @@ +use loco_rs::cli; +use migration::Migrator; +use todolist::app::App; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + cli::main::().await?; + Ok(()) +} diff --git a/examples/loco_example/src/controllers/mod.rs b/examples/loco_example/src/controllers/mod.rs new file mode 100644 index 0000000000..9080a9a2ff --- /dev/null +++ b/examples/loco_example/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod notes; diff --git a/examples/loco_example/src/controllers/notes.rs b/examples/loco_example/src/controllers/notes.rs new file mode 100644 index 0000000000..44ba66ff1f --- /dev/null +++ b/examples/loco_example/src/controllers/notes.rs @@ -0,0 +1,69 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::notes::{ActiveModel, Entity, Model}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + pub title: Option, + pub content: Option, +} + +impl Params { + fn update(&self, item: &mut ActiveModel) { + item.title = Set(self.title.clone()); + item.content = Set(self.content.clone()); + } +} + +async fn load_item(ctx: &AppContext, id: i32) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +pub async fn list(State(ctx): State) -> Result { + format::json(Entity::find().all(&ctx.db).await?) +} + +pub async fn add(State(ctx): State, Json(params): Json) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + let item = item.insert(&ctx.db).await?; + format::json(item) +} + +pub async fn update( + Path(id): Path, + State(ctx): State, + Json(params): Json, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + let item = item.update(&ctx.db).await?; + format::json(item) +} + +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +pub async fn get_one(Path(id): Path, State(ctx): State) -> Result { + format::json(load_item(&ctx, id).await?) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("notes") + .add("/", get(list)) + .add("/", post(add)) + .add("/{id}", get(get_one)) + .add("/{id}", delete(remove)) + .add("/{id}", post(update)) +} diff --git a/examples/loco_example/src/fixtures/notes.yaml b/examples/loco_example/src/fixtures/notes.yaml new file mode 100644 index 0000000000..b0bd4f9dd0 --- /dev/null +++ b/examples/loco_example/src/fixtures/notes.yaml @@ -0,0 +1,11 @@ +--- +- id: 1 + title: Loco note 1 + content: Loco note 1 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + title: Loco note 1 + content: Loco note 1 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/loco_example/src/lib.rs b/examples/loco_example/src/lib.rs new file mode 100644 index 0000000000..bc37b203e8 --- /dev/null +++ b/examples/loco_example/src/lib.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod controllers; +pub mod models; diff --git a/examples/loco_example/src/models/_entities/mod.rs b/examples/loco_example/src/models/_entities/mod.rs new file mode 100644 index 0000000000..a268a1c012 --- /dev/null +++ b/examples/loco_example/src/models/_entities/mod.rs @@ -0,0 +1,5 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4 + +pub mod prelude; + +pub mod notes; diff --git a/examples/loco_example/src/models/_entities/notes.rs b/examples/loco_example/src/models/_entities/notes.rs new file mode 100644 index 0000000000..8f8e0f7578 --- /dev/null +++ b/examples/loco_example/src/models/_entities/notes.rs @@ -0,0 +1,16 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "notes")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub title: Option, + pub content: Option, +} diff --git a/examples/loco_example/src/models/_entities/prelude.rs b/examples/loco_example/src/models/_entities/prelude.rs new file mode 100644 index 0000000000..7e8218d067 --- /dev/null +++ b/examples/loco_example/src/models/_entities/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4 + +pub use super::notes::Entity as Notes; diff --git a/examples/loco_example/src/models/mod.rs b/examples/loco_example/src/models/mod.rs new file mode 100644 index 0000000000..f52ac544ae --- /dev/null +++ b/examples/loco_example/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod _entities; +pub mod notes; diff --git a/examples/loco_example/src/models/notes.rs b/examples/loco_example/src/models/notes.rs new file mode 100644 index 0000000000..1102598236 --- /dev/null +++ b/examples/loco_example/src/models/notes.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::notes::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/loco_example/tests/mod.rs b/examples/loco_example/tests/mod.rs new file mode 100644 index 0000000000..e56e882575 --- /dev/null +++ b/examples/loco_example/tests/mod.rs @@ -0,0 +1,3 @@ +mod models; +mod requests; +mod tasks; diff --git a/examples/loco_example/tests/models/mod.rs b/examples/loco_example/tests/models/mod.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/examples/loco_example/tests/models/mod.rs @@ -0,0 +1 @@ + diff --git a/examples/loco_example/tests/requests/mod.rs b/examples/loco_example/tests/requests/mod.rs new file mode 100644 index 0000000000..ffe152711f --- /dev/null +++ b/examples/loco_example/tests/requests/mod.rs @@ -0,0 +1 @@ +mod notes; diff --git a/examples/loco_example/tests/requests/notes.rs b/examples/loco_example/tests/requests/notes.rs new file mode 100644 index 0000000000..22c5fa114d --- /dev/null +++ b/examples/loco_example/tests/requests/notes.rs @@ -0,0 +1,127 @@ +use insta::{assert_debug_snapshot, with_settings}; +use loco_rs::testing; +use migration::Migrator; +use sea_orm::entity::prelude::*; +use serial_test::serial; +use todolist::{app::App, models::_entities::notes::Entity}; + +// TODO: see how to dedup / extract this to app-local test utils +// not to framework, because that would require a runtime dep on insta +macro_rules! configure_insta { + ($($expr:expr),*) => { + let mut settings = insta::Settings::clone_current(); + settings.set_prepend_module_to_snapshot(false); + settings.set_snapshot_suffix("notes_request"); + let _guard = settings.bind_to_scope(); + }; +} + +// Disabled integration test for GitHub CI +/* +#[tokio::test] +#[serial] +async fn can_get_notes() { + configure_insta!(); + + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); + + let notes = request.get("/api/notes").await; + + with_settings!({ + filters => { + let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); + combined_filters + } + }, { + assert_debug_snapshot!( + (notes.status_code(), notes.text()) + ); + }); + }) + .await; +} + +#[tokio::test] +#[serial] +async fn can_add_note() { + configure_insta!(); + + testing::request::(|request, _ctx| async move { + let payload = serde_json::json!({ + "title": "loco", + "content": "loco note test", + }); + + let add_note_request = request.post("/api/notes").json(&payload).await; + + with_settings!({ + filters => { + let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); + combined_filters + } + }, { + assert_debug_snapshot!( + (add_note_request.status_code(), add_note_request.text()) + ); + }); + }) + .await; +} + +#[tokio::test] +#[serial] +async fn can_get_note() { + configure_insta!(); + + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); + + let add_note_request = request.get("/api/notes/1").await; + + with_settings!({ + filters => { + let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); + combined_filters + } + }, { + assert_debug_snapshot!( + (add_note_request.status_code(), add_note_request.text()) + ); + }); + }) + .await; +} + +#[tokio::test] +#[serial] +async fn can_delete_note() { + configure_insta!(); + + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); + + let count_before_delete = Entity::find().all(&ctx.db).await.unwrap().len(); + let delete_note_request = request.delete("/api/notes/1").await; + + with_settings!({ + filters => { + let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); + combined_filters + } + }, { + assert_debug_snapshot!( + (delete_note_request.status_code(), delete_note_request.text()) + ); + }); + + let count_after_delete = Entity::find().all(&ctx.db).await.unwrap().len(); + assert_eq!(count_after_delete, count_before_delete - 1); + }) + .await; +} +*/ diff --git a/examples/loco_example/tests/requests/snapshots/can_add_note@notes_request.snap b/examples/loco_example/tests/requests/snapshots/can_add_note@notes_request.snap new file mode 100644 index 0000000000..03fef48b75 --- /dev/null +++ b/examples/loco_example/tests/requests/snapshots/can_add_note@notes_request.snap @@ -0,0 +1,8 @@ +--- +source: tests/requests/notes.rs +expression: "(add_note_request.status_code(), add_note_request.text())" +--- +( + 200, + "{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"loco\",\"content\":\"loco note test\"}", +) diff --git a/examples/loco_example/tests/requests/snapshots/can_delete_note@notes_request.snap b/examples/loco_example/tests/requests/snapshots/can_delete_note@notes_request.snap new file mode 100644 index 0000000000..3481fc36d8 --- /dev/null +++ b/examples/loco_example/tests/requests/snapshots/can_delete_note@notes_request.snap @@ -0,0 +1,8 @@ +--- +source: tests/requests/notes.rs +expression: "(delete_note_request.status_code(), delete_note_request.text())" +--- +( + 200, + "", +) diff --git a/examples/loco_example/tests/requests/snapshots/can_get_note@notes_request.snap b/examples/loco_example/tests/requests/snapshots/can_get_note@notes_request.snap new file mode 100644 index 0000000000..ae182ee589 --- /dev/null +++ b/examples/loco_example/tests/requests/snapshots/can_get_note@notes_request.snap @@ -0,0 +1,8 @@ +--- +source: tests/requests/notes.rs +expression: "(add_note_request.status_code(), add_note_request.text())" +--- +( + 200, + "{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"}", +) diff --git a/examples/loco_example/tests/requests/snapshots/can_get_notes@notes_request.snap b/examples/loco_example/tests/requests/snapshots/can_get_notes@notes_request.snap new file mode 100644 index 0000000000..c9ec809849 --- /dev/null +++ b/examples/loco_example/tests/requests/snapshots/can_get_notes@notes_request.snap @@ -0,0 +1,8 @@ +--- +source: tests/requests/notes.rs +expression: "(notes.status_code(), notes.text())" +--- +( + 200, + "[{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"},{\"created_at\":\"DATE\",\"updated_at\":\"DATE\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"}]", +) diff --git a/examples/loco_example/tests/tasks/mod.rs b/examples/loco_example/tests/tasks/mod.rs new file mode 100644 index 0000000000..01fbddaa29 --- /dev/null +++ b/examples/loco_example/tests/tasks/mod.rs @@ -0,0 +1 @@ +pub mod seed; diff --git a/examples/loco_example/tests/tasks/seed.rs b/examples/loco_example/tests/tasks/seed.rs new file mode 100644 index 0000000000..c619300fb2 --- /dev/null +++ b/examples/loco_example/tests/tasks/seed.rs @@ -0,0 +1,43 @@ +//! This task implements data seeding functionality for initializing new +//! development/demo environments. +//! +//! # Example +//! +//! Run the task with the following command: +//! ```sh +//! cargo run task +//! ``` +//! +//! To override existing data and reset the data structure, use the following +//! command with the `refresh:true` argument: +//! ```sh +//! cargo run task seed_data refresh:true +//! ``` + +use loco_rs::{db, prelude::*}; +use migration::Migrator; +use todolist::app::App; + +#[allow(clippy::module_name_repetitions)] +pub struct SeedData; +#[async_trait] +impl Task for SeedData { + fn task(&self) -> TaskInfo { + TaskInfo { + name: "seed_data".to_string(), + detail: "Task for seeding data".to_string(), + } + } + async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { + let refresh = vars + .cli_arg("refresh") + .is_ok_and(|refresh| refresh == "true"); + + if refresh { + db::reset::(&app_context.db).await?; + } + let path = std::path::Path::new("src/fixtures"); + db::run_app_seed::(app_context, path).await?; + Ok(()) + } +} diff --git a/examples/loco_seaography/.cargo/config.toml b/examples/loco_seaography/.cargo/config.toml new file mode 100644 index 0000000000..2def59ad76 --- /dev/null +++ b/examples/loco_seaography/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +loco = "run --" +playground = "run --example playground" diff --git a/examples/loco_seaography/.devcontainer/.env b/examples/loco_seaography/.devcontainer/.env new file mode 100644 index 0000000000..1a3ca5b3c1 --- /dev/null +++ b/examples/loco_seaography/.devcontainer/.env @@ -0,0 +1,6 @@ +POSTGRES_DB=loco_app +POSTGRES_USER=loco +POSTGRES_PASSWORD=loco +DATABASE_URL=postgres://loco:loco@db:5432/loco_app +REDIS_URL=redis://redis:6379 +MAILER_HOST=mailer \ No newline at end of file diff --git a/examples/loco_seaography/.devcontainer/Dockerfile b/examples/loco_seaography/.devcontainer/Dockerfile new file mode 100644 index 0000000000..49616a5f86 --- /dev/null +++ b/examples/loco_seaography/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/devcontainers/rust:1 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends postgresql-client \ + && cargo install sea-orm-cli cargo-insta \ + && chown -R vscode /usr/local/cargo + +COPY .env /.env diff --git a/examples/loco_seaography/.devcontainer/devcontainer.json b/examples/loco_seaography/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ddc39eea3c --- /dev/null +++ b/examples/loco_seaography/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "Loco", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "forwardPorts": [ + 3000 + ] +} \ No newline at end of file diff --git a/examples/loco_seaography/.devcontainer/docker-compose.yml b/examples/loco_seaography/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..6688eb2828 --- /dev/null +++ b/examples/loco_seaography/.devcontainer/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3" + +services: + app: + build: + context: . + dockerfile: Dockerfile + command: sleep infinity + networks: + - db + - redis + - mailer + volumes: + - ../..:/workspaces:cached + env_file: + - .env + db: + image: postgres:15.3-alpine + restart: unless-stopped + ports: + - 5432:5432 + networks: + - db + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env + redis: + image: redis:latest + restart: unless-stopped + ports: + - 6379:6379 + networks: + - redis + mailer: + image: mailtutan/mailtutan:latest + restart: unless-stopped + ports: + - 1080:1080 + - 1025:1025 + networks: + - mailer + +volumes: + postgres-data: + +networks: + db: + redis: + mailer: diff --git a/examples/loco_seaography/.github/workflows/ci.yaml b/examples/loco_seaography/.github/workflows/ci.yaml new file mode 100644 index 0000000000..cb4763c9e4 --- /dev/null +++ b/examples/loco_seaography/.github/workflows/ci.yaml @@ -0,0 +1,107 @@ +name: CI +on: + push: + branches: + - master + - main + pull_request: + +env: + RUST_TOOLCHAIN: stable + TOOLCHAIN_PROFILE: minimal + +jobs: + rustfmt: + name: Check Style + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + components: rustfmt + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Run Clippy + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms + + test: + name: Run Tests + runs-on: ubuntu-latest + + permissions: + contents: read + + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - "6379:6379" + postgres: + image: postgres + env: + POSTGRES_DB: postgres_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + # Set health checks to wait until postgres has started + options: --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --all + env: + REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}} + DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test diff --git a/examples/loco_seaography/.gitignore b/examples/loco_seaography/.gitignore new file mode 100644 index 0000000000..7337dd993e --- /dev/null +++ b/examples/loco_seaography/.gitignore @@ -0,0 +1,23 @@ +**/config/local.yaml +**/config/*.local.yaml +**/config/production.yaml + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# include cargo lock +!Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Temporary upload directory +uploads + +# Local SQLite databases +*.db \ No newline at end of file diff --git a/examples/loco_seaography/.rustfmt.toml b/examples/loco_seaography/.rustfmt.toml new file mode 100644 index 0000000000..26e84608fd --- /dev/null +++ b/examples/loco_seaography/.rustfmt.toml @@ -0,0 +1,7 @@ +comment_width = 80 +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +max_width = 100 +use_small_heuristics = "Default" +wrap_comments = true diff --git a/examples/loco_seaography/Cargo.lock b/examples/loco_seaography/Cargo.lock new file mode 100644 index 0000000000..2bbfb98cb1 --- /dev/null +++ b/examples/loco_seaography/Cargo.lock @@ -0,0 +1,5949 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-graphql" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64", + "bytes", + "chrono", + "fnv", + "futures-channel", + "futures-timer", + "futures-util", + "http", + "indexmap", + "lru", + "mime", + "multer", + "num-traits", + "pin-project-lite", + "regex", + "rust_decimal", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-axum" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725874ecfbf399e071150b8619c4071d7b2b7a2f117e173dddef53c6bdb6bb1" +dependencies = [ + "async-graphql", + "axum", + "bytes", + "futures-util", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", +] + +[[package]] +name = "async-graphql-derive" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling 0.20.11", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum 0.26.3", + "syn 2.0.106", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-parser" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" +dependencies = [ + "bytes", + "indexmap", + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "axum-macros", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "axum-test" +version = "17.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum", + "bytes", + "bytesize", + "cookie", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "backon" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "backtrace_printer" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d28de81c708c843640982b66573df0f0168d87e42854b563971f326745aab7" +dependencies = [ + "btparse-stable", + "colored 2.2.0", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "btparse-stable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d75b8252ed252f881d1dc4482ae3c3854df6ee8183c1906bac50ff358f4f89f" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-unit" +version = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "bytesize" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" + +[[package]] +name = "cc" +version = "1.2.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom 7.1.3", + "once_cell", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cruet" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + +[[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 = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "duct" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] + +[[package]] +name = "duct_sh" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8139179d1d133ab7153920ba3812915b17c61e2514a6f98b1fd03f2c07668d1" +dependencies = [ + "duct", +] + +[[package]] +name = "ego-tree" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "english-to-cron" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26fb7377cbec9a94f60428e6e6afbe10c699a14639b4d3d4b67b25c0bbe0806" +dependencies = [ + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[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 = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.9.4", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.3", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.4", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "insta" +version = "1.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +dependencies = [ + "console", + "once_cell", + "pest", + "pest_derive", + "regex", + "serde", + "similar", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lettre" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" +dependencies = [ + "async-trait", + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "nom 8.0.0", + "percent-encoding", + "quoted_printable", + "rustls", + "socket2 0.6.0", + "tokio", + "tokio-rustls", + "url", + "webpki-roots 1.0.2", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "loco-gen" +version = "0.16.3" +source = "git+https://github.com/SeaQL/loco?branch=master#0e2860b7885e6c00e013b3618896a78095809ede" +dependencies = [ + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.14.0", + "duct", + "heck 0.4.1", + "include_dir", + "regex", + "rrgen", + "serde", + "serde_json", + "tera", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "loco-rs" +version = "0.16.3" +source = "git+https://github.com/SeaQL/loco?branch=master#0e2860b7885e6c00e013b3618896a78095809ede" +dependencies = [ + "argon2", + "async-trait", + "axum", + "axum-extra", + "axum-test", + "backtrace_printer", + "byte-unit", + "bytes", + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.13.3", + "dashmap 6.1.0", + "duct", + "duct_sh", + "english-to-cron", + "futures-util", + "heck 0.4.1", + "include_dir", + "ipnetwork", + "jsonwebtoken", + "lettre", + "loco-gen", + "moka", + "notify", + "opendal", + "rand 0.9.2", + "redis", + "regex", + "scraper", + "sea-orm", + "sea-orm-migration", + "semver", + "serde", + "serde_json", + "serde_variant", + "serde_yaml", + "sqlx", + "tera", + "thiserror 1.0.69", + "tokio", + "tokio-cron-scheduler", + "tokio-util", + "toml", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tree-fs", + "ulid", + "uuid", + "validator", +] + +[[package]] +name = "loco_seaography" +version = "0.1.0" +dependencies = [ + "async-graphql-axum", + "async-trait", + "axum", + "chrono", + "eyre", + "include_dir", + "insta", + "lazy_static", + "loco-rs", + "migration", + "rstest", + "sea-orm", + "seaography", + "serde", + "serde_json", + "serial_test", + "tokio", + "tokio-util", + "tower-service", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "loco-rs", + "sea-orm-migration", + "tokio", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +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.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[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 = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.4", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "opendal" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb9838d0575c6dbaf3fcec7255af8d5771996d4af900bbb6fa9a314dec00a1a" +dependencies = [ + "anyhow", + "backon", + "base64", + "bytes", + "chrono", + "futures", + "getrandom 0.2.16", + "http", + "http-body", + "log", + "md-5", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.6", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +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.106", + "version_check", + "yansi", +] + +[[package]] +name = "psm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" +dependencies = [ + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redis" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "reserve-port" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rrgen" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7a7ede035354391a37e42aa4935b3d8921f0ded896d2ce44bb1a3b6dd76bab" +dependencies = [ + "cruet 0.13.3", + "fs-err", + "glob", + "heck 0.4.1", + "regex", + "serde", + "serde_json", + "serde_regex", + "serde_yaml", + "tera", + "thiserror 1.0.69", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.9.2", + "thiserror 2.0.17", +] + +[[package]] +name = "rust_decimal" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" +dependencies = [ + "ahash 0.8.12", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "indexmap", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c0efc234bdf1f073cc9b448da21a22ed0ac7ff0958a0b3bc4994041dc059df" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more 2.0.1", + "futures-util", + "itertools 0.14.0", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum 0.27.2", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "2.0.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50ae78e4442db69930949a8bcf3b82b0e992c8ea389ae2b1d80504029adab5d" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "indoc", + "regex", + "sea-schema", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b6ce0ae263925930d4e0f95e24a5a8b069dccc61a1ef68da26338470d94929" +dependencies = [ + "heck 0.5.0", + "pluralizer", + "proc-macro-crate", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "2.0.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433fa69cd3f8dc754a2dc51106aee10262fec84ac5195ff7d9966dd1c0d467bd" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93948054fb2d208555a96d03d2c887591deb42ffe3210eedbd8a3234c6fb6d34" +dependencies = [ + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217e9422de35f26c16c5f671fce3c075a65e10322068dbc66078428634af6195" +dependencies = [ + "darling 0.20.11", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", + "thiserror 2.0.17", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693f3ac3a10a228afaf3512b122cffc07c57b4269d233c7ff60571ebb4f0dd17" +dependencies = [ + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b363dd21c20fe4d1488819cb2bc7f8d4696c62dd9f39554f97639f54d57dd0ab" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "seaography" +version = "2.0.0-rc.6" +source = "git+https://github.com/SeaQL/seaography.git?branch=main#ee380956016e74531015051a31f889e45337a335" +dependencies = [ + "async-graphql", + "fnv", + "heck 0.4.1", + "itertools 0.12.1", + "lazy_static", + "sea-orm", + "seaography-macros", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "seaography-macros" +version = "2.0.0-rc.6" +source = "git+https://github.com/SeaQL/seaography.git?branch=main#ee380956016e74531015051a31f889e45337a335" +dependencies = [ + "darling 0.21.3", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags 2.9.4", + "cssparser", + "derive_more 0.99.20", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +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 = "serde_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap 5.5.3", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "servo_arc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.106", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.106", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags 2.9.4", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags 2.9.4", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-cron-scheduler" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2594dd7c2abbbafbb1c78d167fd10860dc7bd75f814cb051a1e0d3e796b9702" +dependencies = [ + "chrono", + "cron", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "async-compression", + "bitflags 2.9.4", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tree-fs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6115680fa5fdb99b4ff19c9c3217e75116d2bb0eae82458c4e1818be6a10c7" +dependencies = [ + "rand 0.9.2", + "serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "rand 0.9.2", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling 0.20.11", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[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.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/loco_seaography/Cargo.toml b/examples/loco_seaography/Cargo.toml new file mode 100644 index 0000000000..4f9aed286a --- /dev/null +++ b/examples/loco_seaography/Cargo.toml @@ -0,0 +1,60 @@ +[workspace] + +[package] +edition = "2024" +name = "loco_seaography" +rust-version = "1.85.0" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +loco-rs = { version = "0.16" } +migration = { path = "migration" } + +async-graphql-axum = { version = "7.0" } +async-trait = "0.1.74" +axum = { version = "0.8", features = ["multipart"] } +chrono = "0.4" +eyre = "0.6" +include_dir = "0.7" +lazy_static = { version = "1.4" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.33.0", default-features = false } +tokio-util = "0.7.11" +tower-service = { version = "0.3" } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } +uuid = { version = "1.6.0", features = ["v4"] } +validator = { version = "0.20" } + +[dependencies.sea-orm] +features = [ + "sqlx-sqlite", + "sqlx-postgres", + "runtime-tokio-rustls", + "macros", +] +version = "~2.0.0-rc.37" # sea-orm version + +[dependencies.seaography] +branch = "main" +features = ["graphql-playground", "with-decimal", "with-chrono"] +git = "https://github.com/SeaQL/seaography.git" +version = "~2.0.0-rc.1" # seaography version + +[[bin]] +name = "loco_seaography-cli" +path = "src/bin/main.rs" +required-features = [] + +[dev-dependencies] +insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } +loco-rs = { version = "0.16", features = ["testing"] } +rstest = "0.18.2" +serial_test = "2.0.0" + +[patch.crates-io] +loco-rs = { git = "https://github.com/SeaQL/loco", branch = "master" } diff --git a/examples/loco_seaography/README.md b/examples/loco_seaography/README.md new file mode 100644 index 0000000000..8c6c29d17d --- /dev/null +++ b/examples/loco_seaography/README.md @@ -0,0 +1,11 @@ +# Adding GraphQL Support to Loco with Seaography + +In this tutorial, we would add a GraphQL endpoint with [Seaography](https://github.com/SeaQL/seaography) based on our Loco starter application. + +Read The full tutorial [here](https://www.sea-ql.org/blog/2024-07-01-graphql-support-with-loco-seaography/). + +Read our first tutorial of the series, [Getting Started with Loco & SeaORM](https://www.sea-ql.org/blog/2024-05-28-getting-started-with-loco-seaorm/), if you haven't. + +![Screenshot Query](Screenshot-Query.png) + +![Screenshot Create](Screenshot-Create.png) diff --git a/examples/loco_seaography/Screenshot-Create.png b/examples/loco_seaography/Screenshot-Create.png new file mode 100644 index 0000000000..3132d0a4b3 Binary files /dev/null and b/examples/loco_seaography/Screenshot-Create.png differ diff --git a/examples/loco_seaography/Screenshot-Query.png b/examples/loco_seaography/Screenshot-Query.png new file mode 100644 index 0000000000..9ce3cce4d3 Binary files /dev/null and b/examples/loco_seaography/Screenshot-Query.png differ diff --git a/examples/loco_seaography/config/development.yaml b/examples/loco_seaography/config/development.yaml new file mode 100644 index 0000000000..99698f1d30 --- /dev/null +++ b/examples/loco_seaography/config/development.yaml @@ -0,0 +1,129 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: true + # Enable pretty backtrace (sets RUST_BACKTRACE=1) + pretty_backtrace: true + # Log level, options: trace, debug, info, warn or error. + level: debug + # Define the logging format. options: compact, pretty or Json + format: compact + # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 3000 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Enable Etag cache header middleware + etag: + enable: true + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: false + # Duration time in milliseconds. + timeout: 5000 + cors: + enable: true + # Set the value of the [`Access-Control-Allow-Origin`][mdn] header + # allow_origins: + # - https://loco.rs + # Set the value of the [`Access-Control-Allow-Headers`][mdn] header + # allow_headers: + # - Content-Type + # Set the value of the [`Access-Control-Allow-Methods`][mdn] header + # allow_methods: + # - POST + # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds + # max_age: 3600 + +# Worker Configuration +workers: + # specifies the worker mode. Options: + # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. + # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. + # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. + mode: BackgroundQueue + +# Mailer Configuration. +mailer: + # SMTP mailer configuration. + smtp: + # Enable/Disable smtp mailer. + enable: true + # SMTP server host. e.x localhost, smtp.gmail.com + host: {{ get_env(name="MAILER_HOST", default="localhost") }} + # SMTP server port + port: 1025 + # Use secure connection (SSL/TLS). + secure: false + # auth: + # user: + # password: + +# Initializers Configuration +# initializers: +# oauth2: +# authorization_code: # Authorization code grant type +# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config. +# ... other fields + +# Database Configuration +database: + # Database connection URI + uri: {{ get_env(name="DATABASE_URL", default="sqlite://loco_seaography.db?mode=rwc") }} + # When enabled, the sql query will be logged. + enable_logging: false + # Set the timeout duration when acquiring a connection. + connect_timeout: 500 + # Set the idle duration before closing a connection. + idle_timeout: 500 + # Minimum number of connections for a pool. + min_connections: 1 + # Maximum number of connections for a pool. + max_connections: 1 + # Run migration up when application loaded + auto_migrate: true + # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_truncate: false + # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_recreate: false + +# Redis Configuration +redis: + # Redis connection URI + uri: {{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }} + # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_flush: false + +# Authentication Configuration +auth: + # JWT authentication + jwt: + # Secret key for token generation and verification + secret: pByQUgg4GmXKAqQQvAGo + # Token expiration time in seconds + expiration: 604800 # 7 days + diff --git a/examples/loco_seaography/migration/Cargo.toml b/examples/loco_seaography/migration/Cargo.toml new file mode 100644 index 0000000000..0bfbe98302 --- /dev/null +++ b/examples/loco_seaography/migration/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +loco-rs = { version = "0.16" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dependencies.sea-orm-migration] +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature +] +version = "~2.0.0-rc.37" # sea-orm-migration version + +[patch.crates-io] +loco-rs = { git = "https://github.com/SeaQL/loco", branch = "master" } diff --git a/examples/loco_seaography/migration/README.md b/examples/loco_seaography/migration/README.md new file mode 100644 index 0000000000..b3ea53eb44 --- /dev/null +++ b/examples/loco_seaography/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- migrate generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/loco_seaography/migration/src/lib.rs b/examples/loco_seaography/migration/src/lib.rs new file mode 100644 index 0000000000..f4f27b76b3 --- /dev/null +++ b/examples/loco_seaography/migration/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(elided_lifetimes_in_paths)] +#![allow(clippy::wildcard_imports)] +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_users; +mod m20231103_114510_notes; +mod m20240520_173001_files; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20220101_000001_users::Migration), + Box::new(m20231103_114510_notes::Migration), + Box::new(m20240520_173001_files::Migration), + ] + } +} diff --git a/examples/loco_seaography/migration/src/m20220101_000001_users.rs b/examples/loco_seaography/migration/src/m20220101_000001_users.rs new file mode 100644 index 0000000000..fa7fb7717b --- /dev/null +++ b/examples/loco_seaography/migration/src/m20220101_000001_users.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = table_auto("users") + .col(pk_auto("id")) + .col(uuid("pid")) + .col(string_uniq("email")) + .col(string("password")) + .col(string("api_key").unique_key()) + .col(string("name")) + .col(string_null("reset_token")) + .col(timestamp_null("reset_sent_at")) + .col(string_null("email_verification_token")) + .col(timestamp_null("email_verification_sent_at")) + .col(timestamp_null("email_verified_at")) + .to_owned(); + manager.create_table(table).await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("users").to_owned()) + .await + } +} diff --git a/examples/loco_seaography/migration/src/m20231103_114510_notes.rs b/examples/loco_seaography/migration/src/m20231103_114510_notes.rs new file mode 100644 index 0000000000..04f13f4867 --- /dev/null +++ b/examples/loco_seaography/migration/src/m20231103_114510_notes.rs @@ -0,0 +1,25 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("notes") + .col(pk_auto("id")) + .col(string_null("title")) + .col(string_null("content")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("notes").to_owned()) + .await + } +} diff --git a/examples/loco_seaography/migration/src/m20240520_173001_files.rs b/examples/loco_seaography/migration/src/m20240520_173001_files.rs new file mode 100644 index 0000000000..3162742bdb --- /dev/null +++ b/examples/loco_seaography/migration/src/m20240520_173001_files.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("files") + .col(pk_auto("id")) + .col(integer("notes_id")) + .col(string("file_path")) + .foreign_key( + ForeignKey::create() + .name("FK_files_notes_id") + .from("files", "notes_id") + .to("notes", "id"), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("files").to_owned()) + .await + } +} diff --git a/examples/loco_seaography/migration/src/main.rs b/examples/loco_seaography/migration/src/main.rs new file mode 100644 index 0000000000..f054deaf88 --- /dev/null +++ b/examples/loco_seaography/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[tokio::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/examples/loco_seaography/src/app.rs b/examples/loco_seaography/src/app.rs new file mode 100644 index 0000000000..b4beb5004f --- /dev/null +++ b/examples/loco_seaography/src/app.rs @@ -0,0 +1,88 @@ +use std::path::Path; + +use async_trait::async_trait; +use loco_rs::{ + Result, + app::{AppContext, Hooks, Initializer}, + bgworker::{BackgroundWorker, Queue}, + boot::{BootResult, StartMode, create_app}, + config::Config, + controller::AppRoutes, + db::{self, truncate_table}, + environment::Environment, + task::Tasks, +}; +use migration::Migrator; + +use crate::{ + controllers, initializers, + models::_entities::{notes, users}, + tasks, + workers::downloader::DownloadWorker, +}; + +pub struct App; +#[async_trait] +impl Hooks for App { + fn app_name() -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn app_version() -> String { + format!( + "{} ({})", + env!("CARGO_PKG_VERSION"), + option_env!("BUILD_SHA") + .or(option_env!("GITHUB_SHA")) + .unwrap_or("dev") + ) + } + + async fn boot( + mode: StartMode, + environment: &Environment, + config: Config, + ) -> Result { + create_app::(mode, environment, config).await + } + + async fn initializers(_ctx: &AppContext) -> Result>> { + let initializers: Vec> = + vec![Box::new(initializers::graphql::GraphQLInitializer)]; + + Ok(initializers) + } + + fn routes(_ctx: &AppContext) -> AppRoutes { + AppRoutes::with_default_routes() + .prefix("/api") + .add_route(controllers::notes::routes()) + .add_route(controllers::auth::routes()) + .add_route(controllers::user::routes()) + .add_route(controllers::files::routes()) + .add_route(controllers::graphql::routes()) + } + + async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { + queue.register(DownloadWorker::build(ctx)).await?; + Ok(()) + } + + fn register_tasks(tasks: &mut Tasks) { + tasks.register(tasks::seed::SeedData); + } + + async fn truncate(ctx: &AppContext) -> Result<()> { + let db = &ctx.db; + truncate_table(db, users::Entity).await?; + truncate_table(db, notes::Entity).await?; + Ok(()) + } + + async fn seed(ctx: &AppContext, base: &Path) -> Result<()> { + let db = &ctx.db; + db::seed::(db, &base.join("users.yaml").display().to_string()).await?; + db::seed::(db, &base.join("notes.yaml").display().to_string()).await?; + Ok(()) + } +} diff --git a/examples/loco_seaography/src/bin/main.rs b/examples/loco_seaography/src/bin/main.rs new file mode 100644 index 0000000000..226711dcf0 --- /dev/null +++ b/examples/loco_seaography/src/bin/main.rs @@ -0,0 +1,9 @@ +use loco_rs::cli; +use loco_seaography::app::App; +use migration::Migrator; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + cli::main::().await?; + Ok(()) +} diff --git a/examples/loco_seaography/src/controllers/auth.rs b/examples/loco_seaography/src/controllers/auth.rs new file mode 100644 index 0000000000..fd15dcdce3 --- /dev/null +++ b/examples/loco_seaography/src/controllers/auth.rs @@ -0,0 +1,156 @@ +use axum::debug_handler; +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::{ + mailers::auth::AuthMailer, + models::{ + _entities::users, + users::{LoginParams, RegisterParams}, + }, + views::auth::LoginResponse, +}; +#[derive(Debug, Deserialize, Serialize)] +pub struct VerifyParams { + pub token: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ForgotParams { + pub email: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ResetParams { + pub token: String, + pub password: String, +} + +/// Register function creates a new user with the given parameters and sends a +/// welcome email to the user +#[debug_handler] +async fn register( + State(ctx): State, + Json(params): Json, +) -> Result { + let res = users::Model::create_with_password(&ctx.db, ¶ms).await; + + let user = match res { + Ok(user) => user, + Err(err) => { + tracing::info!( + message = err.to_string(), + user_email = ¶ms.email, + "could not register user", + ); + return format::json(()); + } + }; + + // Skip email verification, all new registrations are considered verified + let _user = user.into_active_model().verified(&ctx.db).await?; + + // Skip sending verification email as we don't have a mail server + /* + let user = user + .into_active_model() + .set_email_verification_sent(&ctx.db) + .await?; + + AuthMailer::send_welcome(&ctx, &user).await?; + */ + + format::json(()) +} + +/// Verify register user. if the user not verified his email, he can't login to +/// the system. +#[debug_handler] +async fn verify( + State(ctx): State, + Json(params): Json, +) -> Result { + let user = users::Model::find_by_verification_token(&ctx.db, ¶ms.token).await?; + + if user.email_verified_at.is_some() { + tracing::info!(pid = user.pid.to_string(), "user already verified"); + } else { + let active_model = user.into_active_model(); + let user = active_model.verified(&ctx.db).await?; + tracing::info!(pid = user.pid.to_string(), "user verified"); + } + + format::json(()) +} + +/// In case the user forgot his password this endpoints generate a forgot token +/// and send email to the user. In case the email not found in our DB, we are +/// returning a valid request for for security reasons (not exposing users DB +/// list). +#[debug_handler] +async fn forgot( + State(ctx): State, + Json(params): Json, +) -> Result { + let Ok(user) = users::Model::find_by_email(&ctx.db, ¶ms.email).await else { + // we don't want to expose our users email. if the email is invalid we still + // returning success to the caller + return format::json(()); + }; + + let user = user + .into_active_model() + .set_forgot_password_sent(&ctx.db) + .await?; + + AuthMailer::forgot_password(&ctx, &user).await?; + + format::json(()) +} + +/// reset user password by the given parameters +#[debug_handler] +async fn reset(State(ctx): State, Json(params): Json) -> Result { + let Ok(user) = users::Model::find_by_reset_token(&ctx.db, ¶ms.token).await else { + // we don't want to expose our users email. if the email is invalid we still + // returning success to the caller + tracing::info!("reset token not found"); + + return format::json(()); + }; + user.into_active_model() + .reset_password(&ctx.db, ¶ms.password) + .await?; + + format::json(()) +} + +/// Creates a user login and returns a token +#[debug_handler] +async fn login(State(ctx): State, Json(params): Json) -> Result { + let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?; + + let valid = user.verify_password(¶ms.password); + + if !valid { + return unauthorized("unauthorized!"); + } + + let jwt_secret = ctx.config.get_jwt_config()?; + + let token = user + .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration) + .or_else(|_| unauthorized("unauthorized!"))?; + + format::json(LoginResponse::new(&user, &token)) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("auth") + .add("/register", post(register)) + .add("/verify", post(verify)) + .add("/login", post(login)) + .add("/forgot", post(forgot)) + .add("/reset", post(reset)) +} diff --git a/examples/loco_seaography/src/controllers/files.rs b/examples/loco_seaography/src/controllers/files.rs new file mode 100644 index 0000000000..4d76da874d --- /dev/null +++ b/examples/loco_seaography/src/controllers/files.rs @@ -0,0 +1,126 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use std::path::PathBuf; + +use axum::{body::Body, debug_handler, extract::Multipart}; +use loco_rs::prelude::*; +use sea_orm::QueryOrder; +use tokio::{fs, io::AsyncWriteExt}; +use tokio_util::io::ReaderStream; + +use crate::models::_entities::files; + +const UPLOAD_DIR: &str = "./uploads"; + +#[debug_handler] +pub async fn upload( + _auth: auth::JWT, + Path(notes_id): Path, + State(ctx): State, + mut multipart: Multipart, +) -> Result { + // Collect all uploaded files + let mut files = Vec::new(); + + // Iterate all files in the POST body + while let Some(field) = multipart.next_field().await.map_err(|err| { + tracing::error!(error = ?err,"could not readd multipart"); + Error::BadRequest("could not readd multipart".into()) + })? { + // Get the file name + let file_name = match field.file_name() { + Some(file_name) => file_name.to_string(), + _ => return Err(Error::BadRequest("file name not found".into())), + }; + + // Get the file content as bytes + let content = field.bytes().await.map_err(|err| { + tracing::error!(error = ?err,"could not readd bytes"); + Error::BadRequest("could not readd bytes".into()) + })?; + + // Create a folder to store the uploaded file + let now = chrono::offset::Local::now() + .format("%Y%m%d_%H%M%S") + .to_string(); + let uuid = uuid::Uuid::new_v4().to_string(); + let folder = format!("{now}_{uuid}"); + let upload_folder = PathBuf::from(UPLOAD_DIR).join(&folder); + fs::create_dir_all(&upload_folder).await?; + + // Write the file into the newly created folder + let path = upload_folder.join(file_name); + let mut f = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + .await?; + f.write_all(&content).await?; + f.flush().await?; + + // Record the file upload in database + let file = files::ActiveModel { + notes_id: ActiveValue::Set(notes_id), + file_path: ActiveValue::Set( + path.strip_prefix(UPLOAD_DIR) + .unwrap() + .to_str() + .unwrap() + .to_string(), + ), + ..Default::default() + } + .insert(&ctx.db) + .await?; + + files.push(file); + } + + format::json(files) +} + +#[debug_handler] +pub async fn list( + _auth: auth::JWT, + Path(notes_id): Path, + State(ctx): State, +) -> Result { + // Fetch all files uploaded for a specific notes + let files = files::Entity::find() + .filter(files::Column::NotesId.eq(notes_id)) + .order_by_asc(files::Column::Id) + .all(&ctx.db) + .await?; + + format::json(files) +} + +#[debug_handler] +pub async fn view( + _auth: auth::JWT, + Path(files_id): Path, + State(ctx): State, +) -> Result { + // Fetch the file info from database + let file = files::Entity::find_by_id(files_id) + .one(&ctx.db) + .await? + .expect("File not found"); + + // Stream the file + let file = fs::File::open(format!("{UPLOAD_DIR}/{}", file.file_path)).await?; + let stream = ReaderStream::new(file); + let body = Body::from_stream(stream); + + Ok(format::render().response().body(body)?) +} + +pub fn routes() -> Routes { + // Bind the routes + Routes::new() + .prefix("files") + .add("/upload/{notes_id}", post(upload)) + .add("/list/{notes_id}", get(list)) + .add("/view/{files_id}", get(view)) +} diff --git a/examples/loco_seaography/src/controllers/graphql.rs b/examples/loco_seaography/src/controllers/graphql.rs new file mode 100644 index 0000000000..70e47aec4f --- /dev/null +++ b/examples/loco_seaography/src/controllers/graphql.rs @@ -0,0 +1,43 @@ +use async_graphql::{ + dynamic::Schema, + http::{GraphQLPlaygroundConfig, playground_source}, +}; +use async_graphql_axum::GraphQLRequest; +use loco_rs::prelude::*; +use seaography::async_graphql; + +// GraphQL playground UI +async fn graphql_playground() -> Result { + // The `GraphQLPlaygroundConfig` take one parameter + // which is the URL of the GraphQL handler: `/api/graphql` + let res = playground_source(GraphQLPlaygroundConfig::new("/api/graphql")); + + Ok(Response::new(res.into())) +} + +async fn graphql_handler( + // _auth: auth::JWT, + State(ctx): State, + gql_req: GraphQLRequest, +) -> Result { + let gql_req = gql_req.into_inner(); + + let schema: Schema = ctx.shared_store.get().ok_or(( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "GraphQL not setup", + ))?; + let res = schema.execute(gql_req).await.into(); + + Ok(res) +} + +pub fn routes() -> Routes { + // Define route + Routes::new() + // We put all GraphQL route behind `graphql` prefix + .prefix("graphql") + // GraphQL playground page is a GET request + .add("/", get(graphql_playground)) + // GraphQL handler is a POST request + .add("/", post(graphql_handler)) +} diff --git a/examples/loco_seaography/src/controllers/mod.rs b/examples/loco_seaography/src/controllers/mod.rs new file mode 100644 index 0000000000..659c34f1de --- /dev/null +++ b/examples/loco_seaography/src/controllers/mod.rs @@ -0,0 +1,5 @@ +pub mod auth; +pub mod files; +pub mod graphql; +pub mod notes; +pub mod user; diff --git a/examples/loco_seaography/src/controllers/notes.rs b/examples/loco_seaography/src/controllers/notes.rs new file mode 100644 index 0000000000..3377158f9a --- /dev/null +++ b/examples/loco_seaography/src/controllers/notes.rs @@ -0,0 +1,75 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use axum::debug_handler; +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::notes::{ActiveModel, Entity, Model}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + pub title: Option, + pub content: Option, +} + +impl Params { + fn update(&self, item: &mut ActiveModel) { + item.title = Set(self.title.clone()); + item.content = Set(self.content.clone()); + } +} + +async fn load_item(ctx: &AppContext, id: i32) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +pub async fn list(State(ctx): State) -> Result { + format::json(Entity::find().all(&ctx.db).await?) +} + +#[debug_handler] +pub async fn add(State(ctx): State, Json(params): Json) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + let item = item.insert(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +pub async fn update( + Path(id): Path, + State(ctx): State, + Json(params): Json, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + let item = item.update(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +#[debug_handler] +pub async fn get_one(Path(id): Path, State(ctx): State) -> Result { + format::json(load_item(&ctx, id).await?) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("notes") + .add("/", get(list)) + .add("/", post(add)) + .add("/{id}", get(get_one)) + .add("/{id}", delete(remove)) + .add("/{id}", post(update)) +} diff --git a/examples/loco_seaography/src/controllers/user.rs b/examples/loco_seaography/src/controllers/user.rs new file mode 100644 index 0000000000..1f432ae9ef --- /dev/null +++ b/examples/loco_seaography/src/controllers/user.rs @@ -0,0 +1,14 @@ +use axum::debug_handler; +use loco_rs::prelude::*; + +use crate::{models::_entities::users, views::user::CurrentResponse}; + +#[debug_handler] +async fn current(auth: auth::JWT, State(ctx): State) -> Result { + let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; + format::json(CurrentResponse::new(&user)) +} + +pub fn routes() -> Routes { + Routes::new().prefix("user").add("/current", get(current)) +} diff --git a/examples/loco_seaography/src/fixtures/notes.yaml b/examples/loco_seaography/src/fixtures/notes.yaml new file mode 100644 index 0000000000..2237af81c5 --- /dev/null +++ b/examples/loco_seaography/src/fixtures/notes.yaml @@ -0,0 +1,11 @@ +--- +- id: 1 + title: Loco note 1 + content: Loco note 1 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + title: Loco note 2 + content: Loco note 2 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/loco_seaography/src/fixtures/users.yaml b/examples/loco_seaography/src/fixtures/users.yaml new file mode 100644 index 0000000000..8b414e4734 --- /dev/null +++ b/examples/loco_seaography/src/fixtures/users.yaml @@ -0,0 +1,17 @@ +--- +- id: 1 + pid: 11111111-1111-1111-1111-111111111111 + email: user1@example.com + password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" + api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758 + name: user1 + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + pid: 22222222-2222-2222-2222-222222222222 + email: user2@example.com + password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" + api_key: lo-153561ca-fa84-4e1b-813a-c62526d0a77e + name: user2 + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/loco_seaography/src/graphql/mod.rs b/examples/loco_seaography/src/graphql/mod.rs new file mode 100644 index 0000000000..5fffdf9887 --- /dev/null +++ b/examples/loco_seaography/src/graphql/mod.rs @@ -0,0 +1 @@ +pub mod query_root; diff --git a/examples/loco_seaography/src/graphql/query_root.rs b/examples/loco_seaography/src/graphql/query_root.rs new file mode 100644 index 0000000000..554de4cb72 --- /dev/null +++ b/examples/loco_seaography/src/graphql/query_root.rs @@ -0,0 +1,25 @@ +use async_graphql::dynamic::*; +use sea_orm::DatabaseConnection; +use seaography::{Builder, BuilderContext, async_graphql}; + +lazy_static::lazy_static! { static ref CONTEXT: BuilderContext = BuilderContext::default(); } + +pub fn schema( + database: DatabaseConnection, + depth: Option, + complexity: Option, +) -> Result { + // Construct GraphQL schema + let builder = Builder::new(&CONTEXT, database.clone()); + // Register SeaORM Entities + let builder = crate::models::_entities::register_entity_modules(builder); + builder + // Maximum depth of the constructed query + .set_depth_limit(depth) + // Maximum complexity of the constructed query + .set_complexity_limit(complexity) + .schema_builder() + // GraphQL schema with database connection + .data(database) + .finish() +} diff --git a/examples/loco_seaography/src/initializers/graphql.rs b/examples/loco_seaography/src/initializers/graphql.rs new file mode 100644 index 0000000000..91ebdb6052 --- /dev/null +++ b/examples/loco_seaography/src/initializers/graphql.rs @@ -0,0 +1,26 @@ +use crate::graphql::query_root; +use async_trait::async_trait; +use axum::Router as AxumRouter; +use loco_rs::prelude::*; + +// Maximum depth of the constructed query +const DEPTH: Option = None; +// Maximum complexity of the constructed query +const COMPLEXITY: Option = None; + +pub struct GraphQLInitializer; + +#[async_trait] +impl Initializer for GraphQLInitializer { + fn name(&self) -> String { + "graphql".to_string() + } + + async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + let schema = query_root::schema(ctx.db.clone(), DEPTH, COMPLEXITY) + .expect("Failed to build GraphQL schema"); + ctx.shared_store.insert(schema); + + Ok(router) + } +} diff --git a/examples/loco_seaography/src/initializers/mod.rs b/examples/loco_seaography/src/initializers/mod.rs new file mode 100644 index 0000000000..50b9335ce0 --- /dev/null +++ b/examples/loco_seaography/src/initializers/mod.rs @@ -0,0 +1 @@ +pub mod graphql; diff --git a/examples/loco_seaography/src/lib.rs b/examples/loco_seaography/src/lib.rs new file mode 100644 index 0000000000..3642f2782a --- /dev/null +++ b/examples/loco_seaography/src/lib.rs @@ -0,0 +1,9 @@ +pub mod app; +pub mod controllers; +pub mod graphql; +pub mod initializers; +pub mod mailers; +pub mod models; +pub mod tasks; +pub mod views; +pub mod workers; diff --git a/examples/loco_seaography/src/mailers/auth.rs b/examples/loco_seaography/src/mailers/auth.rs new file mode 100644 index 0000000000..30bb1bf2f5 --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth.rs @@ -0,0 +1,65 @@ +// auth mailer +#![allow(non_upper_case_globals)] + +use loco_rs::prelude::*; +use serde_json::json; + +use crate::models::users; + +static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome"); +static forgot: Dir<'_> = include_dir!("src/mailers/auth/forgot"); +// #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets +// move on for now. + +#[allow(clippy::module_name_repetitions)] +pub struct AuthMailer {} +impl Mailer for AuthMailer {} +impl AuthMailer { + /// Sending welcome email the the given user + /// + /// # Errors + /// + /// When email sending is failed + pub async fn send_welcome(ctx: &AppContext, user: &users::Model) -> Result<()> { + Self::mail_template( + ctx, + &welcome, + mailer::Args { + to: user.email.to_string(), + locals: json!({ + "name": user.name, + "verifyToken": user.email_verification_token, + "domain": ctx.config.server.full_url() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } + + /// Sending forgot password email + /// + /// # Errors + /// + /// When email sending is failed + pub async fn forgot_password(ctx: &AppContext, user: &users::Model) -> Result<()> { + Self::mail_template( + ctx, + &forgot, + mailer::Args { + to: user.email.to_string(), + locals: json!({ + "name": user.name, + "resetToken": user.reset_token, + "domain": ctx.config.server.full_url() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } +} diff --git a/examples/loco_seaography/src/mailers/auth/forgot/html.t b/examples/loco_seaography/src/mailers/auth/forgot/html.t new file mode 100644 index 0000000000..221dd60205 --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth/forgot/html.t @@ -0,0 +1,11 @@ +; + + + Hey {{name}}, + Forgot your password? No worries! You can reset it by clicking the link below: + Reset Your Password + If you didn't request a password reset, please ignore this email. + Best regards,
The Loco Team
+ + + diff --git a/examples/loco_seaography/src/mailers/auth/forgot/subject.t b/examples/loco_seaography/src/mailers/auth/forgot/subject.t new file mode 100644 index 0000000000..4938df1e30 --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth/forgot/subject.t @@ -0,0 +1 @@ +Your reset password link diff --git a/examples/loco_seaography/src/mailers/auth/forgot/text.t b/examples/loco_seaography/src/mailers/auth/forgot/text.t new file mode 100644 index 0000000000..58c30fd8d1 --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth/forgot/text.t @@ -0,0 +1,3 @@ +Reset your password with this link: + +http://localhost/reset#{{resetToken}} diff --git a/examples/loco_seaography/src/mailers/auth/welcome/html.t b/examples/loco_seaography/src/mailers/auth/welcome/html.t new file mode 100644 index 0000000000..ae4c41c654 --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth/welcome/html.t @@ -0,0 +1,13 @@ +; + + + Dear {{name}}, + Welcome to Loco! You can now log in to your account. + Before you get started, please verify your account by clicking the link below: + + Verify Your Account + +

Best regards,
The Loco Team

+ + + diff --git a/examples/loco_seaography/src/mailers/auth/welcome/subject.t b/examples/loco_seaography/src/mailers/auth/welcome/subject.t new file mode 100644 index 0000000000..82cc6fbf7b --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth/welcome/subject.t @@ -0,0 +1 @@ +Welcome {{name}} diff --git a/examples/loco_seaography/src/mailers/auth/welcome/text.t b/examples/loco_seaography/src/mailers/auth/welcome/text.t new file mode 100644 index 0000000000..63beefd565 --- /dev/null +++ b/examples/loco_seaography/src/mailers/auth/welcome/text.t @@ -0,0 +1,4 @@ +Welcome {{name}}, you can now log in. + Verify your account with the link below: + + http://localhost/verify#{{verifyToken}} diff --git a/examples/loco_seaography/src/mailers/mod.rs b/examples/loco_seaography/src/mailers/mod.rs new file mode 100644 index 0000000000..0e4a05d597 --- /dev/null +++ b/examples/loco_seaography/src/mailers/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/examples/loco_seaography/src/models/_entities/files.rs b/examples/loco_seaography/src/models/_entities/files.rs new file mode 100644 index 0000000000..186b0cb912 --- /dev/null +++ b/examples/loco_seaography/src/models/_entities/files.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "files")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub notes_id: i32, + pub file_path: String, + #[sea_orm(belongs_to, from = "notes_id", to = "id")] + pub notes: HasOne, +} diff --git a/examples/loco_seaography/src/models/_entities/mod.rs b/examples/loco_seaography/src/models/_entities/mod.rs new file mode 100644 index 0000000000..23ec9cc0bd --- /dev/null +++ b/examples/loco_seaography/src/models/_entities/mod.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5 + +pub mod prelude; + +pub mod files; +pub mod notes; +pub mod users; + +seaography::register_entity_modules!([files, notes, users]); diff --git a/examples/loco_seaography/src/models/_entities/notes.rs b/examples/loco_seaography/src/models/_entities/notes.rs new file mode 100644 index 0000000000..f3325af0c2 --- /dev/null +++ b/examples/loco_seaography/src/models/_entities/notes.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "notes")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub title: Option, + pub content: Option, + #[sea_orm(has_many)] + pub files: HasMany, +} diff --git a/examples/loco_seaography/src/models/_entities/prelude.rs b/examples/loco_seaography/src/models/_entities/prelude.rs new file mode 100644 index 0000000000..109d94d772 --- /dev/null +++ b/examples/loco_seaography/src/models/_entities/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5 + +pub use super::{files::Entity as Files, notes::Entity as Notes, users::Entity as Users}; diff --git a/examples/loco_seaography/src/models/_entities/users.rs b/examples/loco_seaography/src/models/_entities/users.rs new file mode 100644 index 0000000000..39930492ec --- /dev/null +++ b/examples/loco_seaography/src/models/_entities/users.rs @@ -0,0 +1,31 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub pid: Uuid, + #[sea_orm(unique)] + pub email: String, + pub password: String, + #[sea_orm(unique)] + pub api_key: String, + pub name: String, + pub reset_token: Option, + pub reset_sent_at: Option, + pub email_verification_token: Option, + pub email_verification_sent_at: Option, + pub email_verified_at: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] +pub enum RelatedEntity {} diff --git a/examples/loco_seaography/src/models/files.rs b/examples/loco_seaography/src/models/files.rs new file mode 100644 index 0000000000..6e043ce0a3 --- /dev/null +++ b/examples/loco_seaography/src/models/files.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::files::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/loco_seaography/src/models/mod.rs b/examples/loco_seaography/src/models/mod.rs new file mode 100644 index 0000000000..882ae054a2 --- /dev/null +++ b/examples/loco_seaography/src/models/mod.rs @@ -0,0 +1,4 @@ +pub mod _entities; +pub mod files; +pub mod notes; +pub mod users; diff --git a/examples/loco_seaography/src/models/notes.rs b/examples/loco_seaography/src/models/notes.rs new file mode 100644 index 0000000000..1102598236 --- /dev/null +++ b/examples/loco_seaography/src/models/notes.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::notes::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/loco_seaography/src/models/users.rs b/examples/loco_seaography/src/models/users.rs new file mode 100644 index 0000000000..b303c554d6 --- /dev/null +++ b/examples/loco_seaography/src/models/users.rs @@ -0,0 +1,271 @@ +use async_trait::async_trait; +use chrono::offset::Local; +use loco_rs::{auth::jwt, hash, prelude::*}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub use super::_entities::users::{self, ActiveModel, Entity, Model}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginParams { + pub email: String, + pub password: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RegisterParams { + pub email: String, + pub password: String, + pub name: String, +} + +#[derive(Debug, Validate, Deserialize)] +pub struct Validator { + #[validate(length(min = 2, message = "Name must be at least 2 characters long."))] + pub name: String, + #[validate(email(message = "invalid email"))] + pub email: String, +} + +impl Validatable for super::_entities::users::ActiveModel { + fn validator(&self) -> Box { + Box::new(Validator { + name: self.name.as_ref().to_owned(), + email: self.email.as_ref().to_owned(), + }) + } +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for super::_entities::users::ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> Result + where + C: ConnectionTrait, + { + self.validate()?; + if insert { + let mut this = self; + this.pid = ActiveValue::Set(Uuid::new_v4()); + this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4())); + Ok(this) + } else { + Ok(self) + } + } +} + +#[async_trait] +impl Authenticable for super::_entities::users::Model { + async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ApiKey.eq(api_key)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult { + Self::find_by_pid(db, claims_key).await + } +} + +impl super::_entities::users::Model { + /// finds a user by the provided email + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::Email.eq(email)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided verification token + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_verification_token( + db: &DatabaseConnection, + token: &str, + ) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::EmailVerificationToken.eq(token)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// /// finds a user by the provided reset token + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ResetToken.eq(token)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided pid + /// + /// # Errors + /// + /// When could not find user or DB query error + pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult { + let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?; + let user = users::Entity::find() + .filter(users::Column::Pid.eq(parse_uuid)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided api key + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ApiKey.eq(api_key)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// Verifies whether the provided plain password matches the hashed password + /// + /// # Errors + /// + /// when could not verify password + #[must_use] + pub fn verify_password(&self, password: &str) -> bool { + hash::verify_password(password, &self.password) + } + + /// Asynchronously creates a user with a password and saves it to the + /// database. + /// + /// # Errors + /// + /// When could not save the user into the DB + pub async fn create_with_password( + db: &DatabaseConnection, + params: &RegisterParams, + ) -> ModelResult { + let txn = db.begin().await?; + + if users::Entity::find() + .filter(users::Column::Email.eq(¶ms.email)) + .one(&txn) + .await? + .is_some() + { + return Err(ModelError::EntityAlreadyExists {}); + } + + let password_hash = + hash::hash_password(¶ms.password).map_err(|e| ModelError::Any(e.into()))?; + let user = users::ActiveModel { + email: ActiveValue::set(params.email.to_string()), + password: ActiveValue::set(password_hash), + name: ActiveValue::set(params.name.to_string()), + ..Default::default() + } + .insert(&txn) + .await?; + + txn.commit().await?; + + Ok(user) + } + + /// Creates a JWT + /// + /// # Errors + /// + /// when could not convert user claims to jwt token + pub fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult { + Ok(jwt::JWT::new(secret).generate_token( + *expiration, + self.pid.to_string(), + Default::default(), + )?) + } +} + +impl super::_entities::users::ActiveModel { + /// Sets the email verification information for the user and + /// updates it in the database. + /// + /// This method is used to record the timestamp when the email verification + /// was sent and generate a unique verification token for the user. + /// + /// # Errors + /// + /// when has DB query error + pub async fn set_email_verification_sent( + mut self, + db: &DatabaseConnection, + ) -> ModelResult { + self.email_verification_sent_at = ActiveValue::set(Some(Local::now().naive_local())); + self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); + Ok(self.update(db).await?) + } + + /// Sets the information for a reset password request, + /// generates a unique reset password token, and updates it in the + /// database. + /// + /// This method records the timestamp when the reset password token is sent + /// and generates a unique token for the user. + /// + /// # Arguments + /// + /// # Errors + /// + /// when has DB query error + pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult { + self.reset_sent_at = ActiveValue::set(Some(Local::now().naive_local())); + self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); + Ok(self.update(db).await?) + } + + /// Records the verification time when a user verifies their + /// email and updates it in the database. + /// + /// This method sets the timestamp when the user successfully verifies their + /// email. + /// + /// # Errors + /// + /// when has DB query error + pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult { + self.email_verified_at = ActiveValue::set(Some(Local::now().naive_local())); + Ok(self.update(db).await?) + } + + /// Resets the current user password with a new password and + /// updates it in the database. + /// + /// This method hashes the provided password and sets it as the new password + /// for the user. + /// # Errors + /// + /// when has DB query error or could not hashed the given password + pub async fn reset_password( + mut self, + db: &DatabaseConnection, + password: &str, + ) -> ModelResult { + self.password = + ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?); + Ok(self.update(db).await?) + } +} diff --git a/examples/loco_seaography/src/tasks/mod.rs b/examples/loco_seaography/src/tasks/mod.rs new file mode 100644 index 0000000000..01fbddaa29 --- /dev/null +++ b/examples/loco_seaography/src/tasks/mod.rs @@ -0,0 +1 @@ +pub mod seed; diff --git a/examples/loco_seaography/src/tasks/seed.rs b/examples/loco_seaography/src/tasks/seed.rs new file mode 100644 index 0000000000..4e2544d90d --- /dev/null +++ b/examples/loco_seaography/src/tasks/seed.rs @@ -0,0 +1,45 @@ +//! This task implements data seeding functionality for initializing new +//! development/demo environments. +//! +//! # Example +//! +//! Run the task with the following command: +//! ```sh +//! cargo run task +//! ``` +//! +//! To override existing data and reset the data structure, use the following +//! command with the `refresh:true` argument: +//! ```sh +//! cargo run task seed_data refresh:true +//! ``` + +use loco_rs::{db, prelude::*}; +use migration::Migrator; + +use crate::app::App; + +#[allow(clippy::module_name_repetitions)] +pub struct SeedData; +#[async_trait] +impl Task for SeedData { + fn task(&self) -> TaskInfo { + TaskInfo { + name: "seed_data".to_string(), + detail: "Task for seeding data".to_string(), + } + } + + async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { + let refresh = vars + .cli_arg("refresh") + .is_ok_and(|refresh| refresh == "true"); + + if refresh { + db::reset::(&app_context.db).await?; + } + let path = std::path::Path::new("src/fixtures"); + db::run_app_seed::(app_context, path).await?; + Ok(()) + } +} diff --git a/examples/loco_seaography/src/views/auth.rs b/examples/loco_seaography/src/views/auth.rs new file mode 100644 index 0000000000..2240a5087e --- /dev/null +++ b/examples/loco_seaography/src/views/auth.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::users; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginResponse { + pub token: String, + pub pid: String, + pub name: String, + pub is_verified: bool, +} + +impl LoginResponse { + #[must_use] + pub fn new(user: &users::Model, token: &String) -> Self { + Self { + token: token.to_string(), + pid: user.pid.to_string(), + name: user.name.clone(), + is_verified: user.email_verified_at.is_some(), + } + } +} diff --git a/examples/loco_seaography/src/views/mod.rs b/examples/loco_seaography/src/views/mod.rs new file mode 100644 index 0000000000..f9bae3db2a --- /dev/null +++ b/examples/loco_seaography/src/views/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod user; diff --git a/examples/loco_seaography/src/views/user.rs b/examples/loco_seaography/src/views/user.rs new file mode 100644 index 0000000000..9d830410fb --- /dev/null +++ b/examples/loco_seaography/src/views/user.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::users; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CurrentResponse { + pub pid: String, + pub name: String, + pub email: String, +} + +impl CurrentResponse { + #[must_use] + pub fn new(user: &users::Model) -> Self { + Self { + pid: user.pid.to_string(), + name: user.name.clone(), + email: user.email.clone(), + } + } +} diff --git a/examples/loco_seaography/src/workers/downloader.rs b/examples/loco_seaography/src/workers/downloader.rs new file mode 100644 index 0000000000..8a608bfd72 --- /dev/null +++ b/examples/loco_seaography/src/workers/downloader.rs @@ -0,0 +1,41 @@ +use std::time::Duration; + +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; +use tokio::time::sleep; + +use crate::models::users; + +pub struct DownloadWorker { + pub ctx: AppContext, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct DownloadWorkerArgs { + pub user_guid: String, +} + +#[async_trait] +impl BackgroundWorker for DownloadWorker { + fn build(ctx: &AppContext) -> Self { + Self { ctx: ctx.clone() } + } + + async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> { + // TODO: Some actual work goes here... + println!("================================================"); + println!("Sending payment report to user {}", args.user_guid); + + sleep(Duration::from_millis(2000)).await; + + let all = users::Entity::find() + .all(&self.ctx.db) + .await + .map_err(Box::from)?; + for user in &all { + println!("user: {}", user.id); + } + println!("================================================"); + Ok(()) + } +} diff --git a/examples/loco_seaography/src/workers/mod.rs b/examples/loco_seaography/src/workers/mod.rs new file mode 100644 index 0000000000..acb5733da2 --- /dev/null +++ b/examples/loco_seaography/src/workers/mod.rs @@ -0,0 +1 @@ +pub mod downloader; diff --git a/examples/loco_starter/.cargo/config.toml b/examples/loco_starter/.cargo/config.toml new file mode 100644 index 0000000000..2def59ad76 --- /dev/null +++ b/examples/loco_starter/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +loco = "run --" +playground = "run --example playground" diff --git a/examples/loco_starter/.devcontainer/.env b/examples/loco_starter/.devcontainer/.env new file mode 100644 index 0000000000..1a3ca5b3c1 --- /dev/null +++ b/examples/loco_starter/.devcontainer/.env @@ -0,0 +1,6 @@ +POSTGRES_DB=loco_app +POSTGRES_USER=loco +POSTGRES_PASSWORD=loco +DATABASE_URL=postgres://loco:loco@db:5432/loco_app +REDIS_URL=redis://redis:6379 +MAILER_HOST=mailer \ No newline at end of file diff --git a/examples/loco_starter/.devcontainer/Dockerfile b/examples/loco_starter/.devcontainer/Dockerfile new file mode 100644 index 0000000000..49616a5f86 --- /dev/null +++ b/examples/loco_starter/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/devcontainers/rust:1 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends postgresql-client \ + && cargo install sea-orm-cli cargo-insta \ + && chown -R vscode /usr/local/cargo + +COPY .env /.env diff --git a/examples/loco_starter/.devcontainer/devcontainer.json b/examples/loco_starter/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ddc39eea3c --- /dev/null +++ b/examples/loco_starter/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "Loco", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "forwardPorts": [ + 3000 + ] +} \ No newline at end of file diff --git a/examples/loco_starter/.devcontainer/docker-compose.yml b/examples/loco_starter/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..6688eb2828 --- /dev/null +++ b/examples/loco_starter/.devcontainer/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3" + +services: + app: + build: + context: . + dockerfile: Dockerfile + command: sleep infinity + networks: + - db + - redis + - mailer + volumes: + - ../..:/workspaces:cached + env_file: + - .env + db: + image: postgres:15.3-alpine + restart: unless-stopped + ports: + - 5432:5432 + networks: + - db + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env + redis: + image: redis:latest + restart: unless-stopped + ports: + - 6379:6379 + networks: + - redis + mailer: + image: mailtutan/mailtutan:latest + restart: unless-stopped + ports: + - 1080:1080 + - 1025:1025 + networks: + - mailer + +volumes: + postgres-data: + +networks: + db: + redis: + mailer: diff --git a/examples/loco_starter/.github/workflows/ci.yaml b/examples/loco_starter/.github/workflows/ci.yaml new file mode 100644 index 0000000000..cb4763c9e4 --- /dev/null +++ b/examples/loco_starter/.github/workflows/ci.yaml @@ -0,0 +1,107 @@ +name: CI +on: + push: + branches: + - master + - main + pull_request: + +env: + RUST_TOOLCHAIN: stable + TOOLCHAIN_PROFILE: minimal + +jobs: + rustfmt: + name: Check Style + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + components: rustfmt + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Run Clippy + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms + + test: + name: Run Tests + runs-on: ubuntu-latest + + permissions: + contents: read + + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - "6379:6379" + postgres: + image: postgres + env: + POSTGRES_DB: postgres_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + # Set health checks to wait until postgres has started + options: --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --all + env: + REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}} + DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test diff --git a/examples/loco_starter/.gitignore b/examples/loco_starter/.gitignore new file mode 100644 index 0000000000..fb3bd4bbe1 --- /dev/null +++ b/examples/loco_starter/.gitignore @@ -0,0 +1,19 @@ +**/config/local.yaml +**/config/*.local.yaml +**/config/production.yaml + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# include cargo lock +!Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +uploads diff --git a/examples/loco_starter/.rustfmt.toml b/examples/loco_starter/.rustfmt.toml new file mode 100644 index 0000000000..26e84608fd --- /dev/null +++ b/examples/loco_starter/.rustfmt.toml @@ -0,0 +1,7 @@ +comment_width = 80 +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +max_width = 100 +use_small_heuristics = "Default" +wrap_comments = true diff --git a/examples/loco_starter/Cargo.lock b/examples/loco_starter/Cargo.lock new file mode 100644 index 0000000000..623a2fa866 --- /dev/null +++ b/examples/loco_starter/Cargo.lock @@ -0,0 +1,5683 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "axum-test" +version = "17.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum", + "bytes", + "bytesize", + "cookie", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "backon" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "backtrace_printer" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d28de81c708c843640982b66573df0f0168d87e42854b563971f326745aab7" +dependencies = [ + "btparse-stable", + "colored 2.2.0", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "btparse-stable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d75b8252ed252f881d1dc4482ae3c3854df6ee8183c1906bac50ff358f4f89f" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-unit" +version = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytesize" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" + +[[package]] +name = "cc" +version = "1.2.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom 7.1.3", + "once_cell", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cruet" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + +[[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 = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "duct" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] + +[[package]] +name = "duct_sh" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8139179d1d133ab7153920ba3812915b17c61e2514a6f98b1fd03f2c07668d1" +dependencies = [ + "duct", +] + +[[package]] +name = "ego-tree" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "english-to-cron" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26fb7377cbec9a94f60428e6e6afbe10c699a14639b4d3d4b67b25c0bbe0806" +dependencies = [ + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[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 = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.9.4", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.3", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.4", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "insta" +version = "1.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +dependencies = [ + "console", + "once_cell", + "pest", + "pest_derive", + "regex", + "serde", + "similar", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lettre" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" +dependencies = [ + "async-trait", + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "nom 8.0.0", + "percent-encoding", + "quoted_printable", + "rustls", + "socket2 0.6.0", + "tokio", + "tokio-rustls", + "url", + "webpki-roots 1.0.2", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "loco-gen" +version = "0.16.3" +source = "git+https://github.com/SeaQL/loco?branch=master#0e2860b7885e6c00e013b3618896a78095809ede" +dependencies = [ + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.14.0", + "duct", + "heck 0.4.1", + "include_dir", + "regex", + "rrgen", + "serde", + "serde_json", + "tera", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "loco-rs" +version = "0.16.3" +source = "git+https://github.com/SeaQL/loco?branch=master#0e2860b7885e6c00e013b3618896a78095809ede" +dependencies = [ + "argon2", + "async-trait", + "axum", + "axum-extra", + "axum-test", + "backtrace_printer", + "byte-unit", + "bytes", + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.13.3", + "dashmap 6.1.0", + "duct", + "duct_sh", + "english-to-cron", + "futures-util", + "heck 0.4.1", + "include_dir", + "ipnetwork", + "jsonwebtoken", + "lettre", + "loco-gen", + "moka", + "notify", + "opendal", + "rand 0.9.2", + "redis", + "regex", + "scraper", + "sea-orm", + "sea-orm-migration", + "semver", + "serde", + "serde_json", + "serde_variant", + "serde_yaml", + "sqlx", + "tera", + "thiserror 1.0.69", + "tokio", + "tokio-cron-scheduler", + "tokio-util", + "toml", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tree-fs", + "ulid", + "uuid", + "validator", +] + +[[package]] +name = "loco_starter" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "chrono", + "eyre", + "include_dir", + "insta", + "loco-rs", + "migration", + "rstest", + "sea-orm", + "serde", + "serde_json", + "serial_test", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "loco-rs", + "sea-orm-migration", + "tokio", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +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.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[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 = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.4", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "opendal" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb9838d0575c6dbaf3fcec7255af8d5771996d4af900bbb6fa9a314dec00a1a" +dependencies = [ + "anyhow", + "backon", + "base64", + "bytes", + "chrono", + "futures", + "getrandom 0.2.16", + "http", + "http-body", + "log", + "md-5", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.6", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +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.106", + "version_check", + "yansi", +] + +[[package]] +name = "psm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" +dependencies = [ + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redis" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "reserve-port" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rrgen" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7a7ede035354391a37e42aa4935b3d8921f0ded896d2ce44bb1a3b6dd76bab" +dependencies = [ + "cruet 0.13.3", + "fs-err", + "glob", + "heck 0.4.1", + "regex", + "serde", + "serde_json", + "serde_regex", + "serde_yaml", + "tera", + "thiserror 1.0.69", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.9.2", + "thiserror 2.0.17", +] + +[[package]] +name = "rust_decimal" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" +dependencies = [ + "ahash 0.8.12", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "indexmap", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "296a092e16fbbcc3a9969eae3915ef11024b98a43f3b77ff5199bc626c462d7a" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more 2.0.1", + "futures-util", + "itertools", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "2.0.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301f7ace977d940474b47a154a5fc453714d13e5d53bdb077330c6fc6212a31b" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "indoc", + "regex", + "sea-schema", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56ee8be52d15a801dc62a5a30d83dc718762401f7c7945b7f2730ef385b8b3d" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "2.0.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7444940e5b7db32f55dea6d04cfb312480e45ae07753b1391311eccd5753475" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d190945ff6b2914ef0631c1933ef67ee142b15f00b72d40bf241147d24b522" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217e9422de35f26c16c5f671fce3c075a65e10322068dbc66078428634af6195" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", + "thiserror 2.0.17", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68873fa1776b4c25a26e7679f8ee22332978c721168ec1b0b32b6583d5a9381d" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d00bab58be7fcc3a2dbc279099fcd5952d49e45d51444ebe8d5b7e7c4343df3" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags 2.9.4", + "cssparser", + "derive_more 0.99.20", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +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 = "serde_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap 5.5.3", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "servo_arc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.106", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.106", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.9.4", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.9.4", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-cron-scheduler" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2594dd7c2abbbafbb1c78d167fd10860dc7bd75f814cb051a1e0d3e796b9702" +dependencies = [ + "chrono", + "cron", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "async-compression", + "bitflags 2.9.4", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tree-fs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6115680fa5fdb99b4ff19c9c3217e75116d2bb0eae82458c4e1818be6a10c7" +dependencies = [ + "rand 0.9.2", + "serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "rand 0.9.2", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[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.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/loco_starter/Cargo.toml b/examples/loco_starter/Cargo.toml new file mode 100644 index 0000000000..75cb10e2fc --- /dev/null +++ b/examples/loco_starter/Cargo.toml @@ -0,0 +1,51 @@ +[workspace] + +[package] +edition = "2024" +name = "loco_starter" +rust-version = "1.85.0" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +loco-rs = { version = "0.16" } +migration = { path = "migration" } + +async-trait = "0.1.74" +axum = { version = "0.8", features = ["multipart"] } +chrono = "0.4" +eyre = "0.6" +include_dir = "0.7" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.33.0", default-features = false } +tokio-util = "0.7.11" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } +uuid = { version = "1.6.0", features = ["v4"] } +validator = { version = "0.20" } + +[dependencies.sea-orm] +features = [ + "sqlx-sqlite", + "sqlx-postgres", + "runtime-tokio-rustls", + "macros", +] +version = "~2.0.0-rc.37" # sea-orm version + +[[bin]] +name = "loco_starter-cli" +path = "src/bin/main.rs" +required-features = [] + +[dev-dependencies] +insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } +loco-rs = { version = "0.16", features = ["testing"] } +rstest = "0.18.2" +serial_test = "2.0.0" + +[patch.crates-io] +loco-rs = { git = "https://github.com/SeaQL/loco", branch = "master" } diff --git a/examples/loco_starter/README.md b/examples/loco_starter/README.md new file mode 100644 index 0000000000..8add4fcfd6 --- /dev/null +++ b/examples/loco_starter/README.md @@ -0,0 +1,9 @@ +# Getting Started with Loco & SeaORM + +This is an REST notepad backend based on the Loco starter template, with addition of a new REST endpoint to handle file uploads. + +Read The full tutorial [here](https://www.sea-ql.org/blog/2024-05-28-getting-started-with-loco-seaorm/). The documentation of the REST API is available [here](https://documenter.getpostman.com/view/34752358/2sA3QmEF5q). + +![Screenshot App](Screenshot-App.png) + +![Screenshot API](Screenshot-API.png) diff --git a/examples/loco_starter/Screenshot-API.png b/examples/loco_starter/Screenshot-API.png new file mode 100644 index 0000000000..2a3a16df12 Binary files /dev/null and b/examples/loco_starter/Screenshot-API.png differ diff --git a/examples/loco_starter/Screenshot-App.png b/examples/loco_starter/Screenshot-App.png new file mode 100644 index 0000000000..8c25cba8bd Binary files /dev/null and b/examples/loco_starter/Screenshot-App.png differ diff --git a/examples/loco_starter/config/development.yaml b/examples/loco_starter/config/development.yaml new file mode 100644 index 0000000000..4d1837a781 --- /dev/null +++ b/examples/loco_starter/config/development.yaml @@ -0,0 +1,129 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: true + # Enable pretty backtrace (sets RUST_BACKTRACE=1) + pretty_backtrace: true + # Log level, options: trace, debug, info, warn or error. + level: debug + # Define the logging format. options: compact, pretty or Json + format: compact + # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 3000 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Enable Etag cache header middleware + etag: + enable: true + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: false + # Duration time in milliseconds. + timeout: 5000 + cors: + enable: true + # Set the value of the [`Access-Control-Allow-Origin`][mdn] header + # allow_origins: + # - https://loco.rs + # Set the value of the [`Access-Control-Allow-Headers`][mdn] header + # allow_headers: + # - Content-Type + # Set the value of the [`Access-Control-Allow-Methods`][mdn] header + # allow_methods: + # - POST + # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds + # max_age: 3600 + +# Worker Configuration +workers: + # specifies the worker mode. Options: + # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. + # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. + # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. + mode: BackgroundQueue + +# Mailer Configuration. +mailer: + # SMTP mailer configuration. + smtp: + # Enable/Disable smtp mailer. + enable: true + # SMTP server host. e.x localhost, smtp.gmail.com + host: {{ get_env(name="MAILER_HOST", default="localhost") }} + # SMTP server port + port: 1025 + # Use secure connection (SSL/TLS). + secure: false + # auth: + # user: + # password: + +# Initializers Configuration +# initializers: +# oauth2: +# authorization_code: # Authorization code grant type +# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config. +# ... other fields + +# Database Configuration +database: + # Database connection URI + uri: {{ get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/loco_starter_development") }} + # When enabled, the sql query will be logged. + enable_logging: false + # Set the timeout duration when acquiring a connection. + connect_timeout: 500 + # Set the idle duration before closing a connection. + idle_timeout: 500 + # Minimum number of connections for a pool. + min_connections: 1 + # Maximum number of connections for a pool. + max_connections: 1 + # Run migration up when application loaded + auto_migrate: true + # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_truncate: false + # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_recreate: false + +# Redis Configuration +redis: + # Redis connection URI + uri: {{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }} + # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_flush: false + +# Authentication Configuration +auth: + # JWT authentication + jwt: + # Secret key for token generation and verification + secret: pByQUgg4GmXKAqQQvAGo + # Token expiration time in seconds + expiration: 604800 # 7 days + diff --git a/examples/loco_starter/examples/playground.rs b/examples/loco_starter/examples/playground.rs new file mode 100644 index 0000000000..f98b206c34 --- /dev/null +++ b/examples/loco_starter/examples/playground.rs @@ -0,0 +1,22 @@ +use eyre::Context; +#[allow(unused_imports)] +use loco_rs::{cli::playground, prelude::*}; +use loco_starter::app::App; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let _ctx = playground::().await.context("playground")?; + + // let active_model: articles::ActiveModel = ActiveModel { + // title: Set(Some("how to build apps in 3 steps".to_string())), + // content: Set(Some("use Loco: https://loco.rs".to_string())), + // ..Default::default() + // }; + // active_model.insert(&ctx.db).await.unwrap(); + + // let res = articles::Entity::find().all(&ctx.db).await.unwrap(); + // println!("{:?}", res); + println!("welcome to playground. edit me at `examples/playground.rs`"); + + Ok(()) +} diff --git a/examples/loco_starter/migration/Cargo.toml b/examples/loco_starter/migration/Cargo.toml new file mode 100644 index 0000000000..3729f89a98 --- /dev/null +++ b/examples/loco_starter/migration/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +loco-rs = { version = "0.16" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dependencies.sea-orm-migration] +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature +] +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/loco_starter/migration/README.md b/examples/loco_starter/migration/README.md new file mode 100644 index 0000000000..b3ea53eb44 --- /dev/null +++ b/examples/loco_starter/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- migrate generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/loco_starter/migration/src/lib.rs b/examples/loco_starter/migration/src/lib.rs new file mode 100644 index 0000000000..f4f27b76b3 --- /dev/null +++ b/examples/loco_starter/migration/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(elided_lifetimes_in_paths)] +#![allow(clippy::wildcard_imports)] +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_users; +mod m20231103_114510_notes; +mod m20240520_173001_files; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20220101_000001_users::Migration), + Box::new(m20231103_114510_notes::Migration), + Box::new(m20240520_173001_files::Migration), + ] + } +} diff --git a/examples/loco_starter/migration/src/m20220101_000001_users.rs b/examples/loco_starter/migration/src/m20220101_000001_users.rs new file mode 100644 index 0000000000..fa7fb7717b --- /dev/null +++ b/examples/loco_starter/migration/src/m20220101_000001_users.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = table_auto("users") + .col(pk_auto("id")) + .col(uuid("pid")) + .col(string_uniq("email")) + .col(string("password")) + .col(string("api_key").unique_key()) + .col(string("name")) + .col(string_null("reset_token")) + .col(timestamp_null("reset_sent_at")) + .col(string_null("email_verification_token")) + .col(timestamp_null("email_verification_sent_at")) + .col(timestamp_null("email_verified_at")) + .to_owned(); + manager.create_table(table).await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("users").to_owned()) + .await + } +} diff --git a/examples/loco_starter/migration/src/m20231103_114510_notes.rs b/examples/loco_starter/migration/src/m20231103_114510_notes.rs new file mode 100644 index 0000000000..04f13f4867 --- /dev/null +++ b/examples/loco_starter/migration/src/m20231103_114510_notes.rs @@ -0,0 +1,25 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("notes") + .col(pk_auto("id")) + .col(string_null("title")) + .col(string_null("content")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("notes").to_owned()) + .await + } +} diff --git a/examples/loco_starter/migration/src/m20240520_173001_files.rs b/examples/loco_starter/migration/src/m20240520_173001_files.rs new file mode 100644 index 0000000000..3162742bdb --- /dev/null +++ b/examples/loco_starter/migration/src/m20240520_173001_files.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("files") + .col(pk_auto("id")) + .col(integer("notes_id")) + .col(string("file_path")) + .foreign_key( + ForeignKey::create() + .name("FK_files_notes_id") + .from("files", "notes_id") + .to("notes", "id"), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("files").to_owned()) + .await + } +} diff --git a/examples/loco_starter/migration/src/main.rs b/examples/loco_starter/migration/src/main.rs new file mode 100644 index 0000000000..f054deaf88 --- /dev/null +++ b/examples/loco_starter/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[tokio::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/examples/loco_starter/src/app.rs b/examples/loco_starter/src/app.rs new file mode 100644 index 0000000000..17ebb8fe9e --- /dev/null +++ b/examples/loco_starter/src/app.rs @@ -0,0 +1,80 @@ +use std::path::Path; + +use async_trait::async_trait; +use loco_rs::{ + Result, + app::{AppContext, Hooks}, + bgworker::{BackgroundWorker, Queue}, + boot::{BootResult, StartMode, create_app}, + config::Config, + controller::AppRoutes, + db::{self, truncate_table}, + environment::Environment, + task::Tasks, +}; +use migration::Migrator; + +use crate::{ + controllers, + models::_entities::{notes, users}, + tasks, + workers::downloader::DownloadWorker, +}; + +pub struct App; +#[async_trait] +impl Hooks for App { + fn app_name() -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn app_version() -> String { + format!( + "{} ({})", + env!("CARGO_PKG_VERSION"), + option_env!("BUILD_SHA") + .or(option_env!("GITHUB_SHA")) + .unwrap_or("dev") + ) + } + + async fn boot( + mode: StartMode, + environment: &Environment, + config: Config, + ) -> Result { + create_app::(mode, environment, config).await + } + + fn routes(_ctx: &AppContext) -> AppRoutes { + AppRoutes::with_default_routes() + .prefix("/api") + .add_route(controllers::notes::routes()) + .add_route(controllers::auth::routes()) + .add_route(controllers::user::routes()) + .add_route(controllers::files::routes()) + } + + async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { + queue.register(DownloadWorker::build(ctx)).await?; + Ok(()) + } + + fn register_tasks(tasks: &mut Tasks) { + tasks.register(tasks::seed::SeedData); + } + + async fn truncate(ctx: &AppContext) -> Result<()> { + let db = &ctx.db; + truncate_table(db, users::Entity).await?; + truncate_table(db, notes::Entity).await?; + Ok(()) + } + + async fn seed(ctx: &AppContext, base: &Path) -> Result<()> { + let db = &ctx.db; + db::seed::(db, &base.join("users.yaml").display().to_string()).await?; + db::seed::(db, &base.join("notes.yaml").display().to_string()).await?; + Ok(()) + } +} diff --git a/examples/loco_starter/src/bin/main.rs b/examples/loco_starter/src/bin/main.rs new file mode 100644 index 0000000000..92b46682b6 --- /dev/null +++ b/examples/loco_starter/src/bin/main.rs @@ -0,0 +1,9 @@ +use loco_rs::cli; +use loco_starter::app::App; +use migration::Migrator; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + cli::main::().await?; + Ok(()) +} diff --git a/examples/loco_starter/src/controllers/auth.rs b/examples/loco_starter/src/controllers/auth.rs new file mode 100644 index 0000000000..fd15dcdce3 --- /dev/null +++ b/examples/loco_starter/src/controllers/auth.rs @@ -0,0 +1,156 @@ +use axum::debug_handler; +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::{ + mailers::auth::AuthMailer, + models::{ + _entities::users, + users::{LoginParams, RegisterParams}, + }, + views::auth::LoginResponse, +}; +#[derive(Debug, Deserialize, Serialize)] +pub struct VerifyParams { + pub token: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ForgotParams { + pub email: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ResetParams { + pub token: String, + pub password: String, +} + +/// Register function creates a new user with the given parameters and sends a +/// welcome email to the user +#[debug_handler] +async fn register( + State(ctx): State, + Json(params): Json, +) -> Result { + let res = users::Model::create_with_password(&ctx.db, ¶ms).await; + + let user = match res { + Ok(user) => user, + Err(err) => { + tracing::info!( + message = err.to_string(), + user_email = ¶ms.email, + "could not register user", + ); + return format::json(()); + } + }; + + // Skip email verification, all new registrations are considered verified + let _user = user.into_active_model().verified(&ctx.db).await?; + + // Skip sending verification email as we don't have a mail server + /* + let user = user + .into_active_model() + .set_email_verification_sent(&ctx.db) + .await?; + + AuthMailer::send_welcome(&ctx, &user).await?; + */ + + format::json(()) +} + +/// Verify register user. if the user not verified his email, he can't login to +/// the system. +#[debug_handler] +async fn verify( + State(ctx): State, + Json(params): Json, +) -> Result { + let user = users::Model::find_by_verification_token(&ctx.db, ¶ms.token).await?; + + if user.email_verified_at.is_some() { + tracing::info!(pid = user.pid.to_string(), "user already verified"); + } else { + let active_model = user.into_active_model(); + let user = active_model.verified(&ctx.db).await?; + tracing::info!(pid = user.pid.to_string(), "user verified"); + } + + format::json(()) +} + +/// In case the user forgot his password this endpoints generate a forgot token +/// and send email to the user. In case the email not found in our DB, we are +/// returning a valid request for for security reasons (not exposing users DB +/// list). +#[debug_handler] +async fn forgot( + State(ctx): State, + Json(params): Json, +) -> Result { + let Ok(user) = users::Model::find_by_email(&ctx.db, ¶ms.email).await else { + // we don't want to expose our users email. if the email is invalid we still + // returning success to the caller + return format::json(()); + }; + + let user = user + .into_active_model() + .set_forgot_password_sent(&ctx.db) + .await?; + + AuthMailer::forgot_password(&ctx, &user).await?; + + format::json(()) +} + +/// reset user password by the given parameters +#[debug_handler] +async fn reset(State(ctx): State, Json(params): Json) -> Result { + let Ok(user) = users::Model::find_by_reset_token(&ctx.db, ¶ms.token).await else { + // we don't want to expose our users email. if the email is invalid we still + // returning success to the caller + tracing::info!("reset token not found"); + + return format::json(()); + }; + user.into_active_model() + .reset_password(&ctx.db, ¶ms.password) + .await?; + + format::json(()) +} + +/// Creates a user login and returns a token +#[debug_handler] +async fn login(State(ctx): State, Json(params): Json) -> Result { + let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?; + + let valid = user.verify_password(¶ms.password); + + if !valid { + return unauthorized("unauthorized!"); + } + + let jwt_secret = ctx.config.get_jwt_config()?; + + let token = user + .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration) + .or_else(|_| unauthorized("unauthorized!"))?; + + format::json(LoginResponse::new(&user, &token)) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("auth") + .add("/register", post(register)) + .add("/verify", post(verify)) + .add("/login", post(login)) + .add("/forgot", post(forgot)) + .add("/reset", post(reset)) +} diff --git a/examples/loco_starter/src/controllers/files.rs b/examples/loco_starter/src/controllers/files.rs new file mode 100644 index 0000000000..4d76da874d --- /dev/null +++ b/examples/loco_starter/src/controllers/files.rs @@ -0,0 +1,126 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use std::path::PathBuf; + +use axum::{body::Body, debug_handler, extract::Multipart}; +use loco_rs::prelude::*; +use sea_orm::QueryOrder; +use tokio::{fs, io::AsyncWriteExt}; +use tokio_util::io::ReaderStream; + +use crate::models::_entities::files; + +const UPLOAD_DIR: &str = "./uploads"; + +#[debug_handler] +pub async fn upload( + _auth: auth::JWT, + Path(notes_id): Path, + State(ctx): State, + mut multipart: Multipart, +) -> Result { + // Collect all uploaded files + let mut files = Vec::new(); + + // Iterate all files in the POST body + while let Some(field) = multipart.next_field().await.map_err(|err| { + tracing::error!(error = ?err,"could not readd multipart"); + Error::BadRequest("could not readd multipart".into()) + })? { + // Get the file name + let file_name = match field.file_name() { + Some(file_name) => file_name.to_string(), + _ => return Err(Error::BadRequest("file name not found".into())), + }; + + // Get the file content as bytes + let content = field.bytes().await.map_err(|err| { + tracing::error!(error = ?err,"could not readd bytes"); + Error::BadRequest("could not readd bytes".into()) + })?; + + // Create a folder to store the uploaded file + let now = chrono::offset::Local::now() + .format("%Y%m%d_%H%M%S") + .to_string(); + let uuid = uuid::Uuid::new_v4().to_string(); + let folder = format!("{now}_{uuid}"); + let upload_folder = PathBuf::from(UPLOAD_DIR).join(&folder); + fs::create_dir_all(&upload_folder).await?; + + // Write the file into the newly created folder + let path = upload_folder.join(file_name); + let mut f = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + .await?; + f.write_all(&content).await?; + f.flush().await?; + + // Record the file upload in database + let file = files::ActiveModel { + notes_id: ActiveValue::Set(notes_id), + file_path: ActiveValue::Set( + path.strip_prefix(UPLOAD_DIR) + .unwrap() + .to_str() + .unwrap() + .to_string(), + ), + ..Default::default() + } + .insert(&ctx.db) + .await?; + + files.push(file); + } + + format::json(files) +} + +#[debug_handler] +pub async fn list( + _auth: auth::JWT, + Path(notes_id): Path, + State(ctx): State, +) -> Result { + // Fetch all files uploaded for a specific notes + let files = files::Entity::find() + .filter(files::Column::NotesId.eq(notes_id)) + .order_by_asc(files::Column::Id) + .all(&ctx.db) + .await?; + + format::json(files) +} + +#[debug_handler] +pub async fn view( + _auth: auth::JWT, + Path(files_id): Path, + State(ctx): State, +) -> Result { + // Fetch the file info from database + let file = files::Entity::find_by_id(files_id) + .one(&ctx.db) + .await? + .expect("File not found"); + + // Stream the file + let file = fs::File::open(format!("{UPLOAD_DIR}/{}", file.file_path)).await?; + let stream = ReaderStream::new(file); + let body = Body::from_stream(stream); + + Ok(format::render().response().body(body)?) +} + +pub fn routes() -> Routes { + // Bind the routes + Routes::new() + .prefix("files") + .add("/upload/{notes_id}", post(upload)) + .add("/list/{notes_id}", get(list)) + .add("/view/{files_id}", get(view)) +} diff --git a/examples/loco_starter/src/controllers/mod.rs b/examples/loco_starter/src/controllers/mod.rs new file mode 100644 index 0000000000..16dc8522e3 --- /dev/null +++ b/examples/loco_starter/src/controllers/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod files; +pub mod notes; +pub mod user; diff --git a/examples/loco_starter/src/controllers/notes.rs b/examples/loco_starter/src/controllers/notes.rs new file mode 100644 index 0000000000..3377158f9a --- /dev/null +++ b/examples/loco_starter/src/controllers/notes.rs @@ -0,0 +1,75 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use axum::debug_handler; +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::notes::{ActiveModel, Entity, Model}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + pub title: Option, + pub content: Option, +} + +impl Params { + fn update(&self, item: &mut ActiveModel) { + item.title = Set(self.title.clone()); + item.content = Set(self.content.clone()); + } +} + +async fn load_item(ctx: &AppContext, id: i32) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +pub async fn list(State(ctx): State) -> Result { + format::json(Entity::find().all(&ctx.db).await?) +} + +#[debug_handler] +pub async fn add(State(ctx): State, Json(params): Json) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + let item = item.insert(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +pub async fn update( + Path(id): Path, + State(ctx): State, + Json(params): Json, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + let item = item.update(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +#[debug_handler] +pub async fn get_one(Path(id): Path, State(ctx): State) -> Result { + format::json(load_item(&ctx, id).await?) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("notes") + .add("/", get(list)) + .add("/", post(add)) + .add("/{id}", get(get_one)) + .add("/{id}", delete(remove)) + .add("/{id}", post(update)) +} diff --git a/examples/loco_starter/src/controllers/user.rs b/examples/loco_starter/src/controllers/user.rs new file mode 100644 index 0000000000..1f432ae9ef --- /dev/null +++ b/examples/loco_starter/src/controllers/user.rs @@ -0,0 +1,14 @@ +use axum::debug_handler; +use loco_rs::prelude::*; + +use crate::{models::_entities::users, views::user::CurrentResponse}; + +#[debug_handler] +async fn current(auth: auth::JWT, State(ctx): State) -> Result { + let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; + format::json(CurrentResponse::new(&user)) +} + +pub fn routes() -> Routes { + Routes::new().prefix("user").add("/current", get(current)) +} diff --git a/examples/loco_starter/src/fixtures/notes.yaml b/examples/loco_starter/src/fixtures/notes.yaml new file mode 100644 index 0000000000..2237af81c5 --- /dev/null +++ b/examples/loco_starter/src/fixtures/notes.yaml @@ -0,0 +1,11 @@ +--- +- id: 1 + title: Loco note 1 + content: Loco note 1 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + title: Loco note 2 + content: Loco note 2 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/loco_starter/src/fixtures/users.yaml b/examples/loco_starter/src/fixtures/users.yaml new file mode 100644 index 0000000000..8b414e4734 --- /dev/null +++ b/examples/loco_starter/src/fixtures/users.yaml @@ -0,0 +1,17 @@ +--- +- id: 1 + pid: 11111111-1111-1111-1111-111111111111 + email: user1@example.com + password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" + api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758 + name: user1 + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + pid: 22222222-2222-2222-2222-222222222222 + email: user2@example.com + password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" + api_key: lo-153561ca-fa84-4e1b-813a-c62526d0a77e + name: user2 + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/loco_starter/src/lib.rs b/examples/loco_starter/src/lib.rs new file mode 100644 index 0000000000..bcd9cbed82 --- /dev/null +++ b/examples/loco_starter/src/lib.rs @@ -0,0 +1,7 @@ +pub mod app; +pub mod controllers; +pub mod mailers; +pub mod models; +pub mod tasks; +pub mod views; +pub mod workers; diff --git a/examples/loco_starter/src/mailers/auth.rs b/examples/loco_starter/src/mailers/auth.rs new file mode 100644 index 0000000000..30bb1bf2f5 --- /dev/null +++ b/examples/loco_starter/src/mailers/auth.rs @@ -0,0 +1,65 @@ +// auth mailer +#![allow(non_upper_case_globals)] + +use loco_rs::prelude::*; +use serde_json::json; + +use crate::models::users; + +static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome"); +static forgot: Dir<'_> = include_dir!("src/mailers/auth/forgot"); +// #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets +// move on for now. + +#[allow(clippy::module_name_repetitions)] +pub struct AuthMailer {} +impl Mailer for AuthMailer {} +impl AuthMailer { + /// Sending welcome email the the given user + /// + /// # Errors + /// + /// When email sending is failed + pub async fn send_welcome(ctx: &AppContext, user: &users::Model) -> Result<()> { + Self::mail_template( + ctx, + &welcome, + mailer::Args { + to: user.email.to_string(), + locals: json!({ + "name": user.name, + "verifyToken": user.email_verification_token, + "domain": ctx.config.server.full_url() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } + + /// Sending forgot password email + /// + /// # Errors + /// + /// When email sending is failed + pub async fn forgot_password(ctx: &AppContext, user: &users::Model) -> Result<()> { + Self::mail_template( + ctx, + &forgot, + mailer::Args { + to: user.email.to_string(), + locals: json!({ + "name": user.name, + "resetToken": user.reset_token, + "domain": ctx.config.server.full_url() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } +} diff --git a/examples/loco_starter/src/mailers/auth/forgot/html.t b/examples/loco_starter/src/mailers/auth/forgot/html.t new file mode 100644 index 0000000000..221dd60205 --- /dev/null +++ b/examples/loco_starter/src/mailers/auth/forgot/html.t @@ -0,0 +1,11 @@ +; + + + Hey {{name}}, + Forgot your password? No worries! You can reset it by clicking the link below: + Reset Your Password + If you didn't request a password reset, please ignore this email. + Best regards,
The Loco Team
+ + + diff --git a/examples/loco_starter/src/mailers/auth/forgot/subject.t b/examples/loco_starter/src/mailers/auth/forgot/subject.t new file mode 100644 index 0000000000..4938df1e30 --- /dev/null +++ b/examples/loco_starter/src/mailers/auth/forgot/subject.t @@ -0,0 +1 @@ +Your reset password link diff --git a/examples/loco_starter/src/mailers/auth/forgot/text.t b/examples/loco_starter/src/mailers/auth/forgot/text.t new file mode 100644 index 0000000000..58c30fd8d1 --- /dev/null +++ b/examples/loco_starter/src/mailers/auth/forgot/text.t @@ -0,0 +1,3 @@ +Reset your password with this link: + +http://localhost/reset#{{resetToken}} diff --git a/examples/loco_starter/src/mailers/auth/welcome/html.t b/examples/loco_starter/src/mailers/auth/welcome/html.t new file mode 100644 index 0000000000..ae4c41c654 --- /dev/null +++ b/examples/loco_starter/src/mailers/auth/welcome/html.t @@ -0,0 +1,13 @@ +; + + + Dear {{name}}, + Welcome to Loco! You can now log in to your account. + Before you get started, please verify your account by clicking the link below: + + Verify Your Account + +

Best regards,
The Loco Team

+ + + diff --git a/examples/loco_starter/src/mailers/auth/welcome/subject.t b/examples/loco_starter/src/mailers/auth/welcome/subject.t new file mode 100644 index 0000000000..82cc6fbf7b --- /dev/null +++ b/examples/loco_starter/src/mailers/auth/welcome/subject.t @@ -0,0 +1 @@ +Welcome {{name}} diff --git a/examples/loco_starter/src/mailers/auth/welcome/text.t b/examples/loco_starter/src/mailers/auth/welcome/text.t new file mode 100644 index 0000000000..63beefd565 --- /dev/null +++ b/examples/loco_starter/src/mailers/auth/welcome/text.t @@ -0,0 +1,4 @@ +Welcome {{name}}, you can now log in. + Verify your account with the link below: + + http://localhost/verify#{{verifyToken}} diff --git a/examples/loco_starter/src/mailers/mod.rs b/examples/loco_starter/src/mailers/mod.rs new file mode 100644 index 0000000000..0e4a05d597 --- /dev/null +++ b/examples/loco_starter/src/mailers/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/examples/loco_starter/src/models/_entities/files.rs b/examples/loco_starter/src/models/_entities/files.rs new file mode 100644 index 0000000000..f3f83fed2b --- /dev/null +++ b/examples/loco_starter/src/models/_entities/files.rs @@ -0,0 +1,29 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "files")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub notes_id: i32, + pub file_path: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::notes::Entity", + from = "Column::NotesId", + to = "super::notes::Column::Id" + )] + Notes, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Notes.def() + } +} diff --git a/examples/loco_starter/src/models/_entities/mod.rs b/examples/loco_starter/src/models/_entities/mod.rs new file mode 100644 index 0000000000..b3f35dd006 --- /dev/null +++ b/examples/loco_starter/src/models/_entities/mod.rs @@ -0,0 +1,5 @@ +pub mod prelude; + +pub mod files; +pub mod notes; +pub mod users; diff --git a/examples/loco_starter/src/models/_entities/notes.rs b/examples/loco_starter/src/models/_entities/notes.rs new file mode 100644 index 0000000000..8f8e0f7578 --- /dev/null +++ b/examples/loco_starter/src/models/_entities/notes.rs @@ -0,0 +1,16 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "notes")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub title: Option, + pub content: Option, +} diff --git a/examples/loco_starter/src/models/_entities/prelude.rs b/examples/loco_starter/src/models/_entities/prelude.rs new file mode 100644 index 0000000000..251b13a6ad --- /dev/null +++ b/examples/loco_starter/src/models/_entities/prelude.rs @@ -0,0 +1 @@ +pub use super::{notes::Entity as Notes, users::Entity as Users}; diff --git a/examples/loco_starter/src/models/_entities/users.rs b/examples/loco_starter/src/models/_entities/users.rs new file mode 100644 index 0000000000..1b89101e2b --- /dev/null +++ b/examples/loco_starter/src/models/_entities/users.rs @@ -0,0 +1,27 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub pid: Uuid, + #[sea_orm(unique)] + pub email: String, + pub password: String, + #[sea_orm(unique)] + pub api_key: String, + pub name: String, + pub reset_token: Option, + pub reset_sent_at: Option, + pub email_verification_token: Option, + pub email_verification_sent_at: Option, + pub email_verified_at: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/examples/loco_starter/src/models/files.rs b/examples/loco_starter/src/models/files.rs new file mode 100644 index 0000000000..6e043ce0a3 --- /dev/null +++ b/examples/loco_starter/src/models/files.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::files::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/loco_starter/src/models/mod.rs b/examples/loco_starter/src/models/mod.rs new file mode 100644 index 0000000000..882ae054a2 --- /dev/null +++ b/examples/loco_starter/src/models/mod.rs @@ -0,0 +1,4 @@ +pub mod _entities; +pub mod files; +pub mod notes; +pub mod users; diff --git a/examples/loco_starter/src/models/notes.rs b/examples/loco_starter/src/models/notes.rs new file mode 100644 index 0000000000..1102598236 --- /dev/null +++ b/examples/loco_starter/src/models/notes.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::notes::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/loco_starter/src/models/users.rs b/examples/loco_starter/src/models/users.rs new file mode 100644 index 0000000000..b303c554d6 --- /dev/null +++ b/examples/loco_starter/src/models/users.rs @@ -0,0 +1,271 @@ +use async_trait::async_trait; +use chrono::offset::Local; +use loco_rs::{auth::jwt, hash, prelude::*}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub use super::_entities::users::{self, ActiveModel, Entity, Model}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginParams { + pub email: String, + pub password: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RegisterParams { + pub email: String, + pub password: String, + pub name: String, +} + +#[derive(Debug, Validate, Deserialize)] +pub struct Validator { + #[validate(length(min = 2, message = "Name must be at least 2 characters long."))] + pub name: String, + #[validate(email(message = "invalid email"))] + pub email: String, +} + +impl Validatable for super::_entities::users::ActiveModel { + fn validator(&self) -> Box { + Box::new(Validator { + name: self.name.as_ref().to_owned(), + email: self.email.as_ref().to_owned(), + }) + } +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for super::_entities::users::ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> Result + where + C: ConnectionTrait, + { + self.validate()?; + if insert { + let mut this = self; + this.pid = ActiveValue::Set(Uuid::new_v4()); + this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4())); + Ok(this) + } else { + Ok(self) + } + } +} + +#[async_trait] +impl Authenticable for super::_entities::users::Model { + async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ApiKey.eq(api_key)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult { + Self::find_by_pid(db, claims_key).await + } +} + +impl super::_entities::users::Model { + /// finds a user by the provided email + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::Email.eq(email)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided verification token + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_verification_token( + db: &DatabaseConnection, + token: &str, + ) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::EmailVerificationToken.eq(token)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// /// finds a user by the provided reset token + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ResetToken.eq(token)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided pid + /// + /// # Errors + /// + /// When could not find user or DB query error + pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult { + let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?; + let user = users::Entity::find() + .filter(users::Column::Pid.eq(parse_uuid)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided api key + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ApiKey.eq(api_key)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// Verifies whether the provided plain password matches the hashed password + /// + /// # Errors + /// + /// when could not verify password + #[must_use] + pub fn verify_password(&self, password: &str) -> bool { + hash::verify_password(password, &self.password) + } + + /// Asynchronously creates a user with a password and saves it to the + /// database. + /// + /// # Errors + /// + /// When could not save the user into the DB + pub async fn create_with_password( + db: &DatabaseConnection, + params: &RegisterParams, + ) -> ModelResult { + let txn = db.begin().await?; + + if users::Entity::find() + .filter(users::Column::Email.eq(¶ms.email)) + .one(&txn) + .await? + .is_some() + { + return Err(ModelError::EntityAlreadyExists {}); + } + + let password_hash = + hash::hash_password(¶ms.password).map_err(|e| ModelError::Any(e.into()))?; + let user = users::ActiveModel { + email: ActiveValue::set(params.email.to_string()), + password: ActiveValue::set(password_hash), + name: ActiveValue::set(params.name.to_string()), + ..Default::default() + } + .insert(&txn) + .await?; + + txn.commit().await?; + + Ok(user) + } + + /// Creates a JWT + /// + /// # Errors + /// + /// when could not convert user claims to jwt token + pub fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult { + Ok(jwt::JWT::new(secret).generate_token( + *expiration, + self.pid.to_string(), + Default::default(), + )?) + } +} + +impl super::_entities::users::ActiveModel { + /// Sets the email verification information for the user and + /// updates it in the database. + /// + /// This method is used to record the timestamp when the email verification + /// was sent and generate a unique verification token for the user. + /// + /// # Errors + /// + /// when has DB query error + pub async fn set_email_verification_sent( + mut self, + db: &DatabaseConnection, + ) -> ModelResult { + self.email_verification_sent_at = ActiveValue::set(Some(Local::now().naive_local())); + self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); + Ok(self.update(db).await?) + } + + /// Sets the information for a reset password request, + /// generates a unique reset password token, and updates it in the + /// database. + /// + /// This method records the timestamp when the reset password token is sent + /// and generates a unique token for the user. + /// + /// # Arguments + /// + /// # Errors + /// + /// when has DB query error + pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult { + self.reset_sent_at = ActiveValue::set(Some(Local::now().naive_local())); + self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); + Ok(self.update(db).await?) + } + + /// Records the verification time when a user verifies their + /// email and updates it in the database. + /// + /// This method sets the timestamp when the user successfully verifies their + /// email. + /// + /// # Errors + /// + /// when has DB query error + pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult { + self.email_verified_at = ActiveValue::set(Some(Local::now().naive_local())); + Ok(self.update(db).await?) + } + + /// Resets the current user password with a new password and + /// updates it in the database. + /// + /// This method hashes the provided password and sets it as the new password + /// for the user. + /// # Errors + /// + /// when has DB query error or could not hashed the given password + pub async fn reset_password( + mut self, + db: &DatabaseConnection, + password: &str, + ) -> ModelResult { + self.password = + ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?); + Ok(self.update(db).await?) + } +} diff --git a/examples/loco_starter/src/tasks/mod.rs b/examples/loco_starter/src/tasks/mod.rs new file mode 100644 index 0000000000..01fbddaa29 --- /dev/null +++ b/examples/loco_starter/src/tasks/mod.rs @@ -0,0 +1 @@ +pub mod seed; diff --git a/examples/loco_starter/src/tasks/seed.rs b/examples/loco_starter/src/tasks/seed.rs new file mode 100644 index 0000000000..4e2544d90d --- /dev/null +++ b/examples/loco_starter/src/tasks/seed.rs @@ -0,0 +1,45 @@ +//! This task implements data seeding functionality for initializing new +//! development/demo environments. +//! +//! # Example +//! +//! Run the task with the following command: +//! ```sh +//! cargo run task +//! ``` +//! +//! To override existing data and reset the data structure, use the following +//! command with the `refresh:true` argument: +//! ```sh +//! cargo run task seed_data refresh:true +//! ``` + +use loco_rs::{db, prelude::*}; +use migration::Migrator; + +use crate::app::App; + +#[allow(clippy::module_name_repetitions)] +pub struct SeedData; +#[async_trait] +impl Task for SeedData { + fn task(&self) -> TaskInfo { + TaskInfo { + name: "seed_data".to_string(), + detail: "Task for seeding data".to_string(), + } + } + + async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { + let refresh = vars + .cli_arg("refresh") + .is_ok_and(|refresh| refresh == "true"); + + if refresh { + db::reset::(&app_context.db).await?; + } + let path = std::path::Path::new("src/fixtures"); + db::run_app_seed::(app_context, path).await?; + Ok(()) + } +} diff --git a/examples/loco_starter/src/views/auth.rs b/examples/loco_starter/src/views/auth.rs new file mode 100644 index 0000000000..2240a5087e --- /dev/null +++ b/examples/loco_starter/src/views/auth.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::users; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginResponse { + pub token: String, + pub pid: String, + pub name: String, + pub is_verified: bool, +} + +impl LoginResponse { + #[must_use] + pub fn new(user: &users::Model, token: &String) -> Self { + Self { + token: token.to_string(), + pid: user.pid.to_string(), + name: user.name.clone(), + is_verified: user.email_verified_at.is_some(), + } + } +} diff --git a/examples/loco_starter/src/views/mod.rs b/examples/loco_starter/src/views/mod.rs new file mode 100644 index 0000000000..f9bae3db2a --- /dev/null +++ b/examples/loco_starter/src/views/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod user; diff --git a/examples/loco_starter/src/views/user.rs b/examples/loco_starter/src/views/user.rs new file mode 100644 index 0000000000..9d830410fb --- /dev/null +++ b/examples/loco_starter/src/views/user.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::users; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CurrentResponse { + pub pid: String, + pub name: String, + pub email: String, +} + +impl CurrentResponse { + #[must_use] + pub fn new(user: &users::Model) -> Self { + Self { + pid: user.pid.to_string(), + name: user.name.clone(), + email: user.email.clone(), + } + } +} diff --git a/examples/loco_starter/src/workers/downloader.rs b/examples/loco_starter/src/workers/downloader.rs new file mode 100644 index 0000000000..8a608bfd72 --- /dev/null +++ b/examples/loco_starter/src/workers/downloader.rs @@ -0,0 +1,41 @@ +use std::time::Duration; + +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; +use tokio::time::sleep; + +use crate::models::users; + +pub struct DownloadWorker { + pub ctx: AppContext, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct DownloadWorkerArgs { + pub user_guid: String, +} + +#[async_trait] +impl BackgroundWorker for DownloadWorker { + fn build(ctx: &AppContext) -> Self { + Self { ctx: ctx.clone() } + } + + async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> { + // TODO: Some actual work goes here... + println!("================================================"); + println!("Sending payment report to user {}", args.user_guid); + + sleep(Duration::from_millis(2000)).await; + + let all = users::Entity::find() + .all(&self.ctx.db) + .await + .map_err(Box::from)?; + for user in &all { + println!("user: {}", user.id); + } + println!("================================================"); + Ok(()) + } +} diff --git a/examples/loco_starter/src/workers/mod.rs b/examples/loco_starter/src/workers/mod.rs new file mode 100644 index 0000000000..acb5733da2 --- /dev/null +++ b/examples/loco_starter/src/workers/mod.rs @@ -0,0 +1 @@ +pub mod downloader; diff --git a/examples/parquet_example/Cargo.toml b/examples/parquet_example/Cargo.toml new file mode 100644 index 0000000000..3c128c179c --- /dev/null +++ b/examples/parquet_example/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +# A separate workspace + +[package] +edition = "2024" +name = "sea-orm-parquet-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies] +env_logger = { version = "0.11" } +fastrand = "2" +log = { version = "0.4" } +parquet = { version = "57", default-features = false, features = ["arrow"] } +tokio = { version = "1", features = ["full"] } + +[dependencies.sea-orm] +features = [ + "sqlx-sqlite", + "runtime-tokio", + "with-arrow", + "with-chrono", + "with-rust_decimal", +] +path = "../../" diff --git a/examples/parquet_example/src/main.rs b/examples/parquet_example/src/main.rs new file mode 100644 index 0000000000..c0024701f4 --- /dev/null +++ b/examples/parquet_example/src/main.rs @@ -0,0 +1,128 @@ +use sea_orm::{ + ArrowSchema, + entity::*, + prelude::{ChronoUtc, Decimal}, + sea_query::prelude::chrono::Timelike, +}; + +use log::info; + +mod measurement { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "measurement", arrow_schema)] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub recorded_at: ChronoDateTimeUtc, + pub sensor_id: i32, + pub temperature: f64, + #[sea_orm(column_type = "Decimal(Some((10, 4)))")] + pub voltage: Decimal, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let env = env_logger::Env::default().filter_or("RUST_LOG", "info,sea_orm=info,sqlx=warn"); + env_logger::Builder::from_env(env).init(); + + // ----------------------------------------------------------------------- + // Step 1: Generate 100 random rows + // ----------------------------------------------------------------------- + let base_ts = ChronoUtc::now(); + let base_ts = base_ts + .with_nanosecond((base_ts.nanosecond() / 1000) * 1000) + .unwrap(); // truncate to microsecond + let mut rng = fastrand::Rng::new(); + + let models: Vec = (1..=100) + .map(|i| { + let offset = std::time::Duration::from_secs(rng.u64(0..86_400)); + let ts = base_ts + offset; + let sensor_id = rng.i32(100..110); + let temperature = -10.0 + rng.f64() * 50.0; // -10 .. 40 °C + let voltage_raw = 30000 + rng.i64(0..5000); // 3.0000 .. 3.5000 + measurement::ActiveModel { + id: Set(i), + recorded_at: Set(ts), + sensor_id: Set(sensor_id), + temperature: Set(temperature), + voltage: Set(Decimal::new(voltage_raw, 4)), + } + }) + .collect(); + + let schema = measurement::Entity::arrow_schema(); + info!("Arrow schema: {schema:?}"); + + // ----------------------------------------------------------------------- + // Step 2: Convert to Arrow RecordBatch and write to Parquet + // ----------------------------------------------------------------------- + let batch = measurement::ActiveModel::to_arrow(&models, &schema)?; + info!( + "RecordBatch: {} rows, {} columns", + batch.num_rows(), + batch.num_columns() + ); + + let parquet_path = "measurements.parquet"; + + { + let file = std::fs::File::create(parquet_path)?; + let mut writer = parquet::arrow::ArrowWriter::try_new(file, schema.into(), None)?; + writer.write(&batch)?; + writer.close()?; + } + info!("Wrote Parquet file: {parquet_path}"); + + // ----------------------------------------------------------------------- + // Step 3: Read the Parquet file back into a RecordBatch + // ----------------------------------------------------------------------- + let file = std::fs::File::open(parquet_path)?; + let reader = + parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder::try_new(file)?.build()?; + + let batches: Vec<_> = reader.collect::>()?; + info!("Read {} batch(es) from Parquet", batches.len()); + + let read_batch = &batches[0]; + assert_eq!(read_batch.num_rows(), 100); + + // Convert back to ActiveModels + let restored = measurement::ActiveModel::from_arrow(read_batch)?; + info!("Restored {} ActiveModels from Parquet", restored.len()); + + for (original, restored) in models.iter().zip(restored.iter()) { + assert_eq!(original, restored, "Roundtrip mismatch"); + } + info!("Parquet roundtrip verified: all rows match."); + + // ----------------------------------------------------------------------- + // Step 4: Dump into SQLite + // ----------------------------------------------------------------------- + + match std::fs::remove_file("measurements.sqlite") { + Ok(_) => (), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => (), + Err(e) => panic!("Failed to remove file: {e}"), + } + + let db = &sea_orm::Database::connect("sqlite://measurements.sqlite?mode=rwc").await?; + + db.get_schema_builder() + .register(measurement::Entity) + .apply(db) + .await?; + info!("SQLite schema created."); + + measurement::Entity::insert_many(restored).exec(db).await?; + info!("Inserted all rows into SQLite."); + + info!("Done!"); + Ok(()) +} diff --git a/examples/poem_example/.env b/examples/poem_example/.env index acd83c0e1f..e3e391d5a2 100644 --- a/examples/poem_example/.env +++ b/examples/poem_example/.env @@ -1,4 +1,3 @@ HOST=127.0.0.1 PORT=8000 -#DATABASE_URL="mysql://root:@localhost/poem_example" -DATABASE_URL="sqlite::memory:" +DATABASE_URL="sqlite://posts.db?mode=rwc" diff --git a/examples/poem_example/Cargo.toml b/examples/poem_example/Cargo.toml index 9024d729c5..eb124fb2b1 100644 --- a/examples/poem_example/Cargo.toml +++ b/examples/poem_example/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "sea-orm-poem-example" -version = "0.1.0" -edition = "2021" +edition = "2024" +name = "sea-orm-poem-example" +rust-version = "1.85.0" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] poem-example-api = { path = "api" } diff --git a/examples/poem_example/README.md b/examples/poem_example/README.md index d0aa6973b7..37df82cefd 100644 --- a/examples/poem_example/README.md +++ b/examples/poem_example/README.md @@ -4,15 +4,27 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock +cd api +cargo test ``` + +Run migration: + +```bash +cargo run -p migration -- up +``` + +Regenerate entity: + +```bash +sea-orm-cli generate entity --output-dir ./entity/src --lib --entity-format dense --with-serde both +``` \ No newline at end of file diff --git a/examples/poem_example/api/Cargo.toml b/examples/poem_example/api/Cargo.toml index 912218ce8e..0450b4e7dd 100644 --- a/examples/poem_example/api/Cargo.toml +++ b/examples/poem_example/api/Cargo.toml @@ -1,15 +1,32 @@ [package] -name = "poem-example-api" -version = "0.1.0" -edition = "2021" +edition = "2024" +name = "poem-example-api" +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -poem-example-core = { path = "../core" } -tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } -poem = { version = "1.2.33", features = ["static-files"] } +dotenvy = "0.15" +entity = { path = "../entity" } +migration = { path = "../migration" } +poem = { version = "1.3.56", features = ["static-files"] } +serde = { version = "1", features = ["derive"] } +tera = "1.19.0" +tokio = { version = "1.29.0", features = ["macros", "rt-multi-thread"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -serde = { version = "1", features = ["derive"] } -tera = "1.8.0" -dotenvy = "0.15" -entity = { path = "../entity" } -migration = { path = "../migration" } + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + # "sqlx-postgres", + # "sqlx-mysql", + "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version + +[dev-dependencies] +poem-example-api = { path = ".", features = ["sqlite"] } + +[features] +sqlite = ["sea-orm/sqlx-sqlite"] diff --git a/examples/poem_example/api/src/lib.rs b/examples/poem_example/api/src/lib.rs index 02c7ba76e5..a89953ea3d 100644 --- a/examples/poem_example/api/src/lib.rs +++ b/examples/poem_example/api/src/lib.rs @@ -1,3 +1,5 @@ +pub mod service; + use std::env; use entity::post; @@ -7,12 +9,10 @@ use poem::error::InternalServerError; use poem::http::StatusCode; use poem::listener::TcpListener; use poem::web::{Data, Form, Html, Path, Query}; -use poem::{get, handler, post, EndpointExt, Error, IntoResponse, Result, Route, Server}; -use poem_example_core::{ - sea_orm::{Database, DatabaseConnection}, - Mutation as MutationCore, Query as QueryCore, -}; +use poem::{EndpointExt, Error, IntoResponse, Result, Route, Server, get, handler, post}; +use sea_orm::{Database, DatabaseConnection}; use serde::Deserialize; +use service::{Mutation, Query as QueryService}; use tera::Tera; const DEFAULT_POSTS_PER_PAGE: u64 = 5; @@ -34,7 +34,7 @@ async fn create(state: Data<&AppState>, form: Form) -> Result, Query(params): Query) -> Result) -> Result { async fn edit(state: Data<&AppState>, Path(id): Path) -> Result { let conn = &state.conn; - let post: post::Model = QueryCore::find_post_by_id(conn, id) + let post: post::Model = QueryService::find_post_by_id(conn, id) .await .map_err(InternalServerError)? .ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))?; @@ -102,7 +102,7 @@ async fn update( let conn = &state.conn; let form = form.0; - MutationCore::update_post_by_id(conn, id, form) + Mutation::update_post_by_id(conn, id, form) .await .map_err(InternalServerError)?; @@ -113,7 +113,7 @@ async fn update( async fn delete(state: Data<&AppState>, Path(id): Path) -> Result { let conn = &state.conn; - MutationCore::delete_post(conn, id) + Mutation::delete_post(conn, id) .await .map_err(InternalServerError)?; @@ -122,7 +122,9 @@ async fn delete(state: Data<&AppState>, Path(id): Path) -> Result std::io::Result<()> { - std::env::set_var("RUST_LOG", "debug"); + unsafe { + std::env::set_var("RUST_LOG", "debug"); + } tracing_subscriber::fmt::init(); // get env vars diff --git a/examples/poem_example/api/src/service/mod.rs b/examples/poem_example/api/src/service/mod.rs new file mode 100644 index 0000000000..4d0b4b4b4e --- /dev/null +++ b/examples/poem_example/api/src/service/mod.rs @@ -0,0 +1,5 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; diff --git a/examples/jsonrpsee_example/core/src/mutation.rs b/examples/poem_example/api/src/service/mutation.rs similarity index 100% rename from examples/jsonrpsee_example/core/src/mutation.rs rename to examples/poem_example/api/src/service/mutation.rs diff --git a/examples/jsonrpsee_example/core/src/query.rs b/examples/poem_example/api/src/service/query.rs similarity index 100% rename from examples/jsonrpsee_example/core/src/query.rs rename to examples/poem_example/api/src/service/query.rs diff --git a/examples/poem_example/api/tests/crud_tests.rs b/examples/poem_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..c678827047 --- /dev/null +++ b/examples/poem_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use entity::post; +use poem_example_api::service::{Mutation, Query}; +use sea_orm::Database; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/poem_example/core/Cargo.toml b/examples/poem_example/core/Cargo.toml deleted file mode 100644 index e0e578458f..0000000000 --- a/examples/poem_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "poem-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-async-std-native-tls", - # "sqlx-mysql", - # "sqlx-postgres", - "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/poem_example/core/src/lib.rs b/examples/poem_example/core/src/lib.rs deleted file mode 100644 index 4a80f23913..0000000000 --- a/examples/poem_example/core/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod mutation; -mod query; - -pub use mutation::*; -pub use query::*; - -pub use sea_orm; diff --git a/examples/poem_example/core/tests/mock.rs b/examples/poem_example/core/tests/mock.rs deleted file mode 100644 index e4b3ef4f5e..0000000000 --- a/examples/poem_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::post; -use poem_example_core::{Mutation, Query}; -use prepare::prepare_mock_db; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/poem_example/core/tests/prepare.rs b/examples/poem_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/poem_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/poem_example/entity/Cargo.toml b/examples/poem_example/entity/Cargo.toml index 7dea367337..9e429a350b 100644 --- a/examples/poem_example/entity/Cargo.toml +++ b/examples/poem_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,5 +13,5 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/poem_example/entity/src/lib.rs b/examples/poem_example/entity/src/lib.rs index e8b6291ac3..b85eeb2ad5 100644 --- a/examples/poem_example/entity/src/lib.rs +++ b/examples/poem_example/entity/src/lib.rs @@ -1 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub mod prelude; + pub mod post; diff --git a/examples/poem_example/entity/src/post.rs b/examples/poem_example/entity/src/post.rs index 62cde830cf..2e8ff73bbd 100644 --- a/examples/poem_example/entity/src/post.rs +++ b/examples/poem_example/entity/src/post.rs @@ -1,8 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] -#[sea_orm(table_name = "posts")] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] @@ -12,7 +15,4 @@ pub struct Model { pub text: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/poem_example/entity/src/prelude.rs b/examples/poem_example/entity/src/prelude.rs new file mode 100644 index 0000000000..0840a758e9 --- /dev/null +++ b/examples/poem_example/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub use super::post::Entity as Post; diff --git a/examples/poem_example/migration/Cargo.toml b/examples/poem_example/migration/Cargo.toml index 37cd12fc79..9b7dfb86c7 100644 --- a/examples/poem_example/migration/Cargo.toml +++ b/examples/poem_example/migration/Cargo.toml @@ -1,21 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-sqlite", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-sqlite", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/poem_example/migration/src/lib.rs b/examples/poem_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/poem_example/migration/src/lib.rs +++ b/examples/poem_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs b/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/poem_example/migration/src/m20220120_000002_seed_posts.rs b/examples/poem_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/poem_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/poem_example/migration/src/main.rs b/examples/poem_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/poem_example/migration/src/main.rs +++ b/examples/poem_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/poem_example/posts.db b/examples/poem_example/posts.db new file mode 100644 index 0000000000..a565d09308 Binary files /dev/null and b/examples/poem_example/posts.db differ diff --git a/examples/proxy_cloudflare_worker_example/.gitignore b/examples/proxy_cloudflare_worker_example/.gitignore new file mode 100644 index 0000000000..3a951ebee1 --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/.gitignore @@ -0,0 +1,5 @@ +target +node_modules +.wrangler +build +dist diff --git a/examples/proxy_cloudflare_worker_example/Cargo.toml b/examples/proxy_cloudflare_worker_example/Cargo.toml new file mode 100644 index 0000000000..2f6ebb3280 --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/Cargo.toml @@ -0,0 +1,53 @@ +[package] +authors = ["Langyo "] +edition = "2024" +name = "sea-orm-proxy-cloudflare-worker-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[workspace] + +[package.metadata.release] +release = false + +# https://github.com/rustwasm/wasm-pack/issues/1247 +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +async-trait = "0.1" +once_cell = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +axum = { version = "0.7", default-features = false, features = ["macros"] } +tower-service = "0.3.2" +worker = { version = "0.3.0", features = ['http', 'axum', "d1"] } +worker-macros = { version = "0.3.0", features = ['http'] } + +chrono = "0.4" +uuid = { version = "1", features = ["v4"] } + +console_error_panic_hook = { version = "0.1" } +gloo = "0.11" +oneshot = "0.1" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = { version = "0.4" } + +sea-orm = { path = "../../", default-features = false, features = [ + "macros", + "proxy", + "with-uuid", + "with-chrono", + "with-json", + "debug-print", +] } + +[patch.crates-io] +# https://github.com/cloudflare/workers-rs/pull/591 +worker = { git = "https://github.com/cloudflare/workers-rs.git", rev = "ff2e6a0fd58b7e7b4b7651aba46e04067597eb03" } diff --git a/examples/proxy_cloudflare_worker_example/README.md b/examples/proxy_cloudflare_worker_example/README.md new file mode 100644 index 0000000000..87632c9fe9 --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/README.md @@ -0,0 +1,13 @@ +# SeaORM Proxy Demo for Cloudflare Workers + +This is a simple Cloudflare worker written in Rust. It uses the `sea-orm` ORM to interact with SQLite that is stored in the Cloudflare D1. It also uses `axum` as the server framework. + +It's inspired by the [Cloudflare Workers Demo with Rust](https://github.com/logankeenan/full-stack-rust-cloudflare-axum). + +## Run + +Make sure you have `npm` and `cargo` installed. Be sure to use the latest version of `nodejs` and `rust`. + +```bash +npx wrangler dev +``` diff --git a/examples/proxy_cloudflare_worker_example/Wrangler.toml b/examples/proxy_cloudflare_worker_example/Wrangler.toml new file mode 100644 index 0000000000..bd2124c28f --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/Wrangler.toml @@ -0,0 +1,12 @@ +compatibility_date = "2024-07-08" +main = "build/worker/shim.mjs" +name = "axum" + +[[d1_databases]] +binding = "test-d1" +database_name = "axumtest" +# Change it if you want to use your own database +database_id = "00000000-0000-0000-0000-000000000000" + +[build] +command = "cargo install -q worker-build && worker-build --release" diff --git a/examples/proxy_cloudflare_worker_example/src/entity.rs b/examples/proxy_cloudflare_worker_example/src/entity.rs new file mode 100644 index 0000000000..8688460464 --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/src/entity.rs @@ -0,0 +1,17 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "posts")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + + pub title: String, + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/proxy_cloudflare_worker_example/src/lib.rs b/examples/proxy_cloudflare_worker_example/src/lib.rs new file mode 100644 index 0000000000..3e0d5c5354 --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/src/lib.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use axum::{body::Body, response::Response}; +use tower_service::Service; +use worker::{Context, Env, HttpRequest, event}; + +pub(crate) mod entity; +pub(crate) mod orm; +pub(crate) mod route; + +// https://developers.cloudflare.com/workers/languages/rust +#[event(fetch)] +async fn fetch(req: HttpRequest, env: Env, _ctx: Context) -> Result> { + console_error_panic_hook::set_once(); + + Ok(route::router(env).call(req).await?) +} diff --git a/examples/proxy_cloudflare_worker_example/src/orm.rs b/examples/proxy_cloudflare_worker_example/src/orm.rs new file mode 100644 index 0000000000..5b367a94e4 --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/src/orm.rs @@ -0,0 +1,218 @@ +use anyhow::{Context, Result, anyhow}; +use std::{collections::BTreeMap, sync::Arc}; +use wasm_bindgen::JsValue; + +use sea_orm::{ + ConnectionTrait, Database, DatabaseConnection, DbBackend, DbErr, ProxyDatabaseTrait, + ProxyExecResult, ProxyRow, RuntimeErr, Schema, Statement, Value, Values, +}; +use worker::{Env, console_log}; + +struct ProxyDb { + env: Arc, +} + +impl std::fmt::Debug for ProxyDb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProxyDb").finish() + } +} + +impl ProxyDb { + async fn do_query(env: Arc, statement: Statement) -> Result> { + let sql = statement.sql.clone(); + let values = match statement.values { + Some(Values(values)) => values + .iter() + .map(|val| match &val { + Value::BigInt(Some(val)) => JsValue::from(val.to_string()), + Value::BigUnsigned(Some(val)) => JsValue::from(val.to_string()), + Value::Int(Some(val)) => JsValue::from(*val), + Value::Unsigned(Some(val)) => JsValue::from(*val), + Value::SmallInt(Some(val)) => JsValue::from(*val), + Value::SmallUnsigned(Some(val)) => JsValue::from(*val), + Value::TinyInt(Some(val)) => JsValue::from(*val), + Value::TinyUnsigned(Some(val)) => JsValue::from(*val), + + Value::Float(Some(val)) => JsValue::from_f64(*val as f64), + Value::Double(Some(val)) => JsValue::from_f64(*val), + + Value::Bool(Some(val)) => JsValue::from(*val), + Value::Bytes(Some(val)) => JsValue::from(format!( + "X'{}'", + val.iter() + .map(|byte| format!("{:02x}", byte)) + .collect::() + )), + Value::Char(Some(val)) => JsValue::from(val.to_string()), + Value::Json(Some(val)) => JsValue::from(val.to_string()), + Value::String(Some(val)) => JsValue::from(val.to_string()), + + Value::ChronoDate(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTime(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTimeLocal(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTimeUtc(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTimeWithTimeZone(Some(val)) => JsValue::from(val.to_string()), + + _ => JsValue::NULL, + }) + .collect(), + None => Vec::new(), + }; + + console_log!("SQL query values: {:?}", values); + let ret = env.d1("test-d1")?.prepare(sql).bind(&values)?.all().await?; + if let Some(message) = ret.error() { + return Err(anyhow!(message.to_string())); + } + + let ret = ret.results::()?; + let ret = ret + .iter() + .map(|row| { + let mut values = BTreeMap::new(); + for (key, value) in row.as_object().unwrap() { + values.insert( + key.clone(), + match &value { + serde_json::Value::Bool(val) => Value::Bool(Some(*val)), + serde_json::Value::Number(val) => { + if val.is_i64() { + Value::BigInt(Some(val.as_i64().unwrap())) + } else if val.is_u64() { + Value::BigUnsigned(Some(val.as_u64().unwrap())) + } else { + Value::Double(Some(val.as_f64().unwrap())) + } + } + serde_json::Value::String(val) => { + Value::String(Some(Box::new(val.clone()))) + } + _ => unreachable!("Unsupported JSON value"), + }, + ); + } + ProxyRow { values } + }) + .collect(); + console_log!("SQL query result: {:?}", ret); + + Ok(ret) + } + + async fn do_execute(env: Arc, statement: Statement) -> Result { + let sql = statement.sql.clone(); + let values = match statement.values { + Some(Values(values)) => values + .iter() + .map(|val| match &val { + Value::BigInt(Some(val)) => JsValue::from(val.to_string()), + Value::BigUnsigned(Some(val)) => JsValue::from(val.to_string()), + Value::Int(Some(val)) => JsValue::from(*val), + Value::Unsigned(Some(val)) => JsValue::from(*val), + Value::SmallInt(Some(val)) => JsValue::from(*val), + Value::SmallUnsigned(Some(val)) => JsValue::from(*val), + Value::TinyInt(Some(val)) => JsValue::from(*val), + Value::TinyUnsigned(Some(val)) => JsValue::from(*val), + + Value::Float(Some(val)) => JsValue::from_f64(*val as f64), + Value::Double(Some(val)) => JsValue::from_f64(*val), + + Value::Bool(Some(val)) => JsValue::from(*val), + Value::Bytes(Some(val)) => JsValue::from(format!( + "X'{}'", + val.iter() + .map(|byte| format!("{:02x}", byte)) + .collect::() + )), + Value::Char(Some(val)) => JsValue::from(val.to_string()), + Value::Json(Some(val)) => JsValue::from(val.to_string()), + Value::String(Some(val)) => JsValue::from(val.to_string()), + + Value::ChronoDate(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTime(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTimeLocal(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTimeUtc(Some(val)) => JsValue::from(val.to_string()), + Value::ChronoDateTimeWithTimeZone(Some(val)) => JsValue::from(val.to_string()), + + _ => JsValue::NULL, + }) + .collect(), + None => Vec::new(), + }; + + let ret = env + .d1("test-d1")? + .prepare(sql) + .bind(&values)? + .run() + .await? + .meta()?; + console_log!("SQL execute result: {:?}", ret); + + let last_insert_id = ret + .as_ref() + .map(|meta| meta.last_row_id.unwrap_or(0)) + .unwrap_or(0) as u64; + let rows_affected = ret + .as_ref() + .map(|meta| meta.rows_written.unwrap_or(0)) + .unwrap_or(0) as u64; + + Ok(ProxyExecResult { + last_insert_id, + rows_affected, + }) + } +} + +#[async_trait::async_trait] +impl ProxyDatabaseTrait for ProxyDb { + async fn query(&self, statement: Statement) -> Result, DbErr> { + console_log!("SQL query: {:?}", statement); + + let env = self.env.clone(); + let (tx, rx) = oneshot::channel(); + wasm_bindgen_futures::spawn_local(async move { + let ret = Self::do_query(env, statement).await; + tx.send(ret).unwrap(); + }); + + let ret = rx.await.unwrap(); + ret.map_err(|err| DbErr::Conn(RuntimeErr::Internal(err.to_string()))) + } + + async fn execute(&self, statement: Statement) -> Result { + console_log!("SQL execute: {:?}", statement); + + let env = self.env.clone(); + let (tx, rx) = oneshot::channel(); + wasm_bindgen_futures::spawn_local(async move { + let ret = Self::do_execute(env, statement).await; + tx.send(ret).unwrap(); + }); + + let ret = rx.await.unwrap(); + ret.map_err(|err| DbErr::Conn(RuntimeErr::Internal(err.to_string()))) + } +} + +pub async fn init_db(env: Arc) -> Result { + let db = Database::connect_proxy(DbBackend::Sqlite, Arc::new(Box::new(ProxyDb { env }))) + .await + .context("Failed to connect to database")?; + let builder = db.get_database_backend(); + + console_log!("Connected to database"); + + db.execute( + builder.build( + Schema::new(builder) + .create_table_from_entity(crate::entity::Entity) + .if_not_exists(), + ), + ) + .await?; + + Ok(db) +} diff --git a/examples/proxy_cloudflare_worker_example/src/route.rs b/examples/proxy_cloudflare_worker_example/src/route.rs new file mode 100644 index 0000000000..ecde40ceba --- /dev/null +++ b/examples/proxy_cloudflare_worker_example/src/route.rs @@ -0,0 +1,90 @@ +use anyhow::Result; +use std::sync::Arc; + +use axum::{Router, extract::State, http::StatusCode, response::IntoResponse, routing::get}; +use worker::{Env, console_error, console_log}; + +use sea_orm::{ + ActiveModelTrait, + ActiveValue::{NotSet, Set}, + EntityTrait, +}; + +#[derive(Clone)] +struct CFEnv { + pub env: Arc, +} + +unsafe impl Send for CFEnv {} +unsafe impl Sync for CFEnv {} + +pub fn router(env: Env) -> Router { + let state = CFEnv { env: Arc::new(env) }; + + Router::new() + .route("/", get(handler_get)) + .route("/generate", get(handler_generate)) + .with_state(state) +} + +async fn handler_get( + State(state): State, +) -> Result { + let env = state.env.clone(); + let db = crate::orm::init_db(env).await.map_err(|err| { + console_log!("Failed to connect to database: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to connect to database".to_string(), + ) + })?; + + let ret = crate::entity::Entity::find() + .all(&db) + .await + .map_err(|err| { + console_log!("Failed to query database: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to query database".to_string(), + ) + })?; + let ret = serde_json::to_string(&ret).map_err(|err| { + console_error!("Failed to serialize response: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to serialize response".to_string(), + ) + })?; + + Ok(ret.into_response()) +} + +async fn handler_generate( + State(state): State, +) -> Result { + let env = state.env.clone(); + let db = crate::orm::init_db(env).await.map_err(|err| { + console_log!("Failed to connect to database: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to connect to database".to_string(), + ) + })?; + + let ret = crate::entity::ActiveModel { + id: NotSet, + title: Set(chrono::Utc::now().to_rfc3339()), + text: Set(uuid::Uuid::new_v4().to_string()), + }; + + let ret = ret.insert(&db).await.map_err(|err| { + console_log!("Failed to insert into database: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to insert into database".to_string(), + ) + })?; + + Ok(format!("Inserted: {:?}", ret).into_response()) +} diff --git a/examples/proxy_gluesql_example/Cargo.toml b/examples/proxy_gluesql_example/Cargo.toml new file mode 100644 index 0000000000..fbd68b9cd1 --- /dev/null +++ b/examples/proxy_gluesql_example/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors = ["Langyo "] +edition = "2024" +name = "sea-orm-proxy-gluesql-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[workspace] + +[dependencies] +async-std = { version = "1.12", features = ["attributes", "tokio1"] } +async-stream = { version = "0.3" } +async-trait = { version = "0.1" } +bigdecimal = { version = "=0.4.9" } +futures = { version = "0.3" } +futures-util = { version = "0.3" } +serde = { version = "1" } +serde_json = { version = "1" } + +gluesql = { version = "0.15", default-features = false, features = [ + "memory-storage", +] } +sea-orm = { path = "../../", default-features = false, features = [ + "macros", + "proxy", + "debug-print", +] } +sqlparser = "0.40" + +[dev-dependencies] +smol = { version = "1.2" } +smol-potat = { version = "1.1" } diff --git a/examples/proxy_gluesql_example/README.md b/examples/proxy_gluesql_example/README.md new file mode 100644 index 0000000000..2269c471eb --- /dev/null +++ b/examples/proxy_gluesql_example/README.md @@ -0,0 +1,7 @@ +# SeaORM Proxy Demo for GlueSQL + +Run this demo for [GlueSQL](https://gluesql.org/) with the following command: + +```bash +cargo run +``` diff --git a/examples/actix3_example/entity/src/lib.rs b/examples/proxy_gluesql_example/src/entity/mod.rs similarity index 100% rename from examples/actix3_example/entity/src/lib.rs rename to examples/proxy_gluesql_example/src/entity/mod.rs diff --git a/examples/actix3_example/entity/src/post.rs b/examples/proxy_gluesql_example/src/entity/post.rs similarity index 82% rename from examples/actix3_example/entity/src/post.rs rename to examples/proxy_gluesql_example/src/entity/post.rs index 62cde830cf..8688460464 100644 --- a/examples/actix3_example/entity/src/post.rs +++ b/examples/proxy_gluesql_example/src/entity/post.rs @@ -5,10 +5,9 @@ use serde::{Deserialize, Serialize}; #[sea_orm(table_name = "posts")] pub struct Model { #[sea_orm(primary_key)] - #[serde(skip_deserializing)] - pub id: i32, + pub id: i64, + pub title: String, - #[sea_orm(column_type = "Text")] pub text: String, } diff --git a/examples/proxy_gluesql_example/src/main.rs b/examples/proxy_gluesql_example/src/main.rs new file mode 100644 index 0000000000..69745a7815 --- /dev/null +++ b/examples/proxy_gluesql_example/src/main.rs @@ -0,0 +1,191 @@ +//! Proxy connection example. + +#![deny(missing_docs)] + +mod entity; + +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; + +use gluesql::{memory_storage::MemoryStorage, prelude::Glue}; +use sea_orm::{ + ActiveValue::Set, Database, DbBackend, DbErr, EntityTrait, ProxyDatabaseTrait, ProxyExecResult, + ProxyRow, Statement, +}; + +use entity::post::{ActiveModel, Entity}; + +struct ProxyDb { + mem: Mutex>, +} + +impl std::fmt::Debug for ProxyDb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProxyDb").finish() + } +} + +#[async_trait::async_trait] +impl ProxyDatabaseTrait for ProxyDb { + async fn query(&self, statement: Statement) -> Result, DbErr> { + println!("SQL query: {:?}", statement); + let sql = statement.sql.clone(); + + let mut ret: Vec = vec![]; + async_std::task::block_on(async { + for payload in self.mem.lock().unwrap().execute(sql).await.unwrap().iter() { + match payload { + gluesql::prelude::Payload::Select { labels, rows } => { + for row in rows.iter() { + let mut map = BTreeMap::new(); + for (label, column) in labels.iter().zip(row.iter()) { + map.insert( + label.to_owned(), + match column { + gluesql::prelude::Value::I64(val) => { + sea_orm::Value::BigInt(Some(*val)) + } + gluesql::prelude::Value::Str(val) => { + sea_orm::Value::String(Some(val.to_owned())) + } + _ => unreachable!("Unsupported value: {:?}", column), + }, + ); + } + ret.push(map.into()); + } + } + _ => unreachable!("Unsupported payload: {:?}", payload), + } + } + }); + + Ok(ret) + } + + async fn execute(&self, statement: Statement) -> Result { + let sql = if let Some(values) = statement.values { + // Replace all the '?' with the statement values + use sqlparser::ast::{Expr, Value}; + use sqlparser::dialect::GenericDialect; + use sqlparser::parser::Parser; + + let dialect = GenericDialect {}; + let mut ast = Parser::parse_sql(&dialect, statement.sql.as_str()).unwrap(); + match &mut ast[0] { + sqlparser::ast::Statement::Insert { + columns, source, .. + } => { + for item in columns.iter_mut() { + item.quote_style = Some('"'); + } + + if let Some(obj) = source { + match &mut *obj.body { + sqlparser::ast::SetExpr::Values(obj) => { + for (mut item, val) in obj.rows[0].iter_mut().zip(values.0.iter()) { + match &mut item { + Expr::Value(item) => { + *item = match val { + sea_orm::Value::String(val) => { + Value::SingleQuotedString(match val { + Some(val) => val.to_string(), + None => "".to_string(), + }) + } + sea_orm::Value::BigInt(val) => Value::Number( + val.unwrap_or(0).to_string(), + false, + ), + _ => todo!(), + }; + } + _ => todo!(), + } + } + } + _ => todo!(), + } + } + } + _ => todo!(), + } + + let statement = &ast[0]; + statement.to_string() + } else { + statement.sql + }; + + println!("SQL execute: {}", sql); + async_std::task::block_on(async { + self.mem.lock().unwrap().execute(sql).await.unwrap(); + }); + + Ok(ProxyExecResult { + last_insert_id: 1, + rows_affected: 1, + }) + } +} + +#[async_std::main] +async fn main() { + let mem = MemoryStorage::default(); + let mut glue = Glue::new(mem); + + glue.execute( + r#" + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + text TEXT NOT NULL + ) + "#, + ) + .await + .unwrap(); + + let db = Database::connect_proxy( + DbBackend::Sqlite, + Arc::new(Box::new(ProxyDb { + mem: Mutex::new(glue), + })), + ) + .await + .unwrap(); + + println!("Initialized"); + + let data = ActiveModel { + id: Set(11), + title: Set("Homo".to_owned()), + text: Set("いいよ、来いよ".to_owned()), + }; + Entity::insert(data).exec(&db).await.unwrap(); + let data = ActiveModel { + id: Set(45), + title: Set("Homo".to_owned()), + text: Set("そうだよ".to_owned()), + }; + Entity::insert(data).exec(&db).await.unwrap(); + let data = ActiveModel { + id: Set(14), + title: Set("Homo".to_owned()), + text: Set("悔い改めて".to_owned()), + }; + Entity::insert(data).exec(&db).await.unwrap(); + + let list = Entity::find().all(&db).await.unwrap().to_vec(); + println!("Result: {:?}", list); +} + +#[cfg(test)] +mod tests { + #[smol_potat::test] + async fn try_run() { + crate::main() + } +} diff --git a/examples/quickstart/Cargo.toml b/examples/quickstart/Cargo.toml new file mode 100644 index 0000000000..ea3dfba255 --- /dev/null +++ b/examples/quickstart/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +# A separate workspace + +[package] +edition = "2024" +name = "sea-orm-quickstart" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies] +env_logger = { version = "0.11" } +log = { version = "0.4" } +tokio = { version = "1", features = ["full"] } + +[dependencies.sea-orm] +features = [ + "sqlx-sqlite", + "runtime-tokio", + "debug-print", + "entity-registry", + "schema-sync", +] +path = "../../" diff --git a/examples/quickstart/README.md b/examples/quickstart/README.md new file mode 100644 index 0000000000..8884821d1d --- /dev/null +++ b/examples/quickstart/README.md @@ -0,0 +1,3 @@ +# SeaORM Quick Start + +This is a single file app that strives to minimize dependency and code noise. It uses an in-memory SQLite database so there is no setup. Just do `cargo run`! \ No newline at end of file diff --git a/examples/quickstart/src/main.rs b/examples/quickstart/src/main.rs new file mode 100644 index 0000000000..a907ebf2bb --- /dev/null +++ b/examples/quickstart/src/main.rs @@ -0,0 +1,460 @@ +use log::info; + +mod user { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "user")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub email: String, + #[sea_orm(has_one)] + pub profile: HasOne, + #[sea_orm(has_many)] + pub posts: HasMany, + #[sea_orm(self_ref, via = "user_follower", from = "User", to = "Follower")] + pub followers: HasMany, + #[sea_orm(self_ref, via = "user_follower", reverse)] + pub following: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod profile { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "profile")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub picture: String, + #[sea_orm(unique)] + pub user_id: i32, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod post { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "post")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub title: String, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub author: HasOne, + #[sea_orm(has_many)] + pub comments: HasMany, + #[sea_orm(has_many, via = "post_tag")] + pub tags: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod comment { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "comment")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub comment: String, + pub user_id: i32, + pub post_id: i32, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, + #[sea_orm(belongs_to, from = "post_id", to = "id")] + pub post: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod tag { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "tag")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub tag: String, + #[sea_orm(has_many, via = "post_tag")] + pub posts: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod post_tag { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] + #[sea_orm(table_name = "post_tag")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub post_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub tag_id: i32, + #[sea_orm(belongs_to, from = "post_id", to = "id")] + pub post: Option, + #[sea_orm(belongs_to, from = "tag_id", to = "id")] + pub tag: Option, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod user_follower { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] + #[sea_orm(table_name = "user_follower")] + pub struct Model { + #[sea_orm(primary_key)] + pub user_id: i32, + #[sea_orm(primary_key)] + pub follower_id: i32, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: Option, + #[sea_orm( + belongs_to, + relation_enum = "Follower", + from = "follower_id", + to = "id" + )] + pub follower: Option, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +#[tokio::main] +async fn main() -> Result<(), sea_orm::DbErr> { + ///// Part 0: Setup Environment ///// + + // This disables sqlx's logging and enables sea-orm's logging with parameter injection, + // which is easier to debug. + let env = env_logger::Env::default().filter_or("RUST_LOG", "info,sea_orm=debug,sqlx=warn"); + env_logger::Builder::from_env(env).init(); + + use sea_orm::{entity::*, query::*}; + + // Use a SQLite in memory database so no setup needed. + // SeaORM supports MySQL, Postgres, SQL Server as well. + let db = &sea_orm::Database::connect("sqlite::memory:").await?; + + // Populate this fresh database with tables. + // + // All entities defined in this crate are automatically registered + // into the schema registry, regardless of which module they live in. + // + // The registry may also include entities from upstream crates, + // so here we restrict it to entities defined in this crate only. + // + // The order of entity definitions does not matter. + // SeaORM resolves foreign key dependencies automatically + // and creates the tables in the correct order with their keys. + db.get_schema_registry("sea_orm_quickstart::*") + .sync(db) + .await?; + + info!("Schema created."); + + ///// Part 1: CRUD with nested 1-1 and 1-N relations ///// + + info!("Create user Bob with a profile:"); + let bob = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("Tennis")) + .insert(db) + .await?; + + info!("Find Bob by email:"); + assert_eq!( + bob, + // this method is generated by #[sea_orm::model] on unique keys + user::Entity::find_by_email("bob@sea-ql.org") + .one(db) + .await? + .unwrap() + ); + + info!("Query user with profile in a single query:"); + let mut bob = user::Entity::load() + .filter_by_id(bob.id) + .with(profile::Entity) + .one(db) + .await? + .expect("Not found"); + assert_eq!(bob.name, "Bob"); + assert_eq!(bob.profile.as_ref().unwrap().picture, "Tennis"); + + // Here we take ownership of the nested model, modify in place and save it + info!("Update Bob's profile:"); + bob.profile + .take() + .unwrap() + .into_active_model() + .set_picture("Landscape") + .save(db) + .await?; + + info!("Confirmed that it's been updated:"); + assert_eq!( + profile::Entity::find_by_user_id(bob.id).all(db).await?[0].picture, + "Landscape" + ); + + // we don't have to set the `user_id` of the posts, they're automatically set to Bob + info!("Bob wrote some posts:"); + let mut bob = bob.into_active_model(); + bob.posts + .push( + post::ActiveModel::builder() + .set_title("Lorem ipsum dolor sit amet, consectetur adipiscing elit"), + ) + .push( + post::ActiveModel::builder() + .set_title("Ut enim ad minim veniam, quis nostrud exercitation"), + ); + bob.save(db).await?; + + info!("Find Bob's profile and his posts:"); + let bob = user::Entity::load() + .filter(user::COLUMN.name.eq("Bob")) + .with(profile::Entity) + .with(post::Entity) + .one(db) + .await? + .unwrap(); + + assert_eq!(bob.name, "Bob"); + assert_eq!(bob.profile.as_ref().unwrap().picture, "Landscape"); + assert!(bob.posts[0].title.starts_with("Lorem ipsum")); + assert!(bob.posts[1].title.starts_with("Ut enim ad")); + + // It's actually fine to create user + profile the other way round. + // SeaORM figures out the dependency and creates the user first. + info!("Create a new user Alice:"); + let alice = profile::ActiveModel::builder() + .set_user( + user::ActiveModel::builder() + .set_name("Alice") + .set_email("alice@rust-lang.org"), + ) + .set_picture("Park") + .insert(db) + .await? + .user + .unwrap(); + + // Not only can we insert new posts via the bob active model, + // we can also add new comments to the posts. + // SeaORM walks the document tree and figures out what's changed, + // and perform the operation in one transaction. + let mut bob = bob.into_active_model(); + info!("Alice commented on Bob's post:"); + bob.posts[0].comments.push( + comment::ActiveModel::builder() + .set_comment("nice post!") + .set_user_id(alice.id), + ); + bob.posts[1].comments.push( + comment::ActiveModel::builder() + .set_comment("interesting!") + .set_user_id(alice.id), + ); + let bob = bob.save(db).await?; + + info!("Find all posts with author along with comments and who commented:"); + let posts = post::Entity::load() + .with(user::Entity) + .with((comment::Entity, user::Entity)) + .all(db) + .await?; + + assert!(posts[0].title.starts_with("Lorem ipsum")); + assert_eq!(posts[0].author.as_ref().unwrap().name, "Bob"); + assert_eq!(posts[0].comments.len(), 1); + assert_eq!(posts[0].comments[0].comment, "nice post!"); + assert_eq!(posts[0].comments[0].user.as_ref().unwrap().name, "Alice"); + + assert!(posts[1].title.starts_with("Ut enim ad")); + assert_eq!(posts[1].author.as_ref().unwrap().name, "Bob"); + assert_eq!(posts[1].comments.len(), 1); + assert_eq!(posts[1].comments[0].comment, "interesting!"); + + // Again, we can apply multiple changes in one operation, + // the queries are executed inside a transaction. + info!("Update post title and comment on first post:"); + let mut post = posts[0].clone().into_active_model(); + post.title = Set("Lorem ipsum dolor sit amet".into()); // shorten it + post.comments[0].comment = Set("nice post! I learnt a lot".into()); + post.save(db).await?; + + info!("Confirm the post and comment is updated"); + let post = post::Entity::load() + .filter_by_id(posts[0].id) + .with(comment::Entity) + .one(db) + .await? + .unwrap(); + + assert_eq!(post.title, "Lorem ipsum dolor sit amet"); + assert_eq!(post.comments[0].comment, "nice post! I learnt a lot"); + + // Comments belongs to post. They will be deleted first, otherwise the foreign key + // would prevent the operation. + info!("Delete the post along with all comments"); + post.delete(db).await?; + + assert!( + post::Entity::find_by_id(posts[0].id) + .one(db) + .await? + .is_none() + ); + + ///// Part 2: managing M-N relations ///// + + // A unique feature of SeaORM is modelling many-to-many relations in a high level way + + info!("Insert one tag for later use"); + let sunny = tag::ActiveModel::builder() + .set_tag("sunny") + .save(db) + .await?; + + info!("Insert a new post with 2 tags"); + let mut post = post::ActiveModel::builder() + .set_title("A perfect day out") + .set_user_id(alice.id) + .add_tag(sunny.clone()) // an existing tag + .add_tag(tag::ActiveModel::builder().set_tag("foodie")) // a new tag + .save(db) // new tag will be created and associcated to the new post + .await?; + + let post_id = post.id.clone().unwrap(); + + { + info!("get back the post and tags"); + let post = post::Entity::load() + .filter_by_id(post_id) + .with(tag::Entity) + .one(db) + .await? + .unwrap(); + assert_eq!(post.title, "A perfect day out"); + assert_eq!(post.tags.len(), 2); + assert_eq!(post.tags[0].tag, "sunny"); + assert_eq!(post.tags[1].tag, "foodie"); + } + + info!("Add new tag to post"); + post.tags + .push(tag::ActiveModel::builder().set_tag("downtown")); + let mut post = post.save(db).await?; + + { + info!("get back the post and tags"); + let post = post::Entity::load() + .filter_by_id(post_id) + .with(tag::Entity) + .one(db) + .await? + .unwrap(); + assert_eq!(post.tags.len(), 3); + assert_eq!(post.tags[0].tag, "sunny"); + assert_eq!(post.tags[1].tag, "foodie"); + assert_eq!(post.tags[2].tag, "downtown"); + } + + info!("Update post title and remove a tag"); + let mut tags = post.tags.take(); + tags.as_mut_vec().remove(0); // it actually rained + post.title = Set("Almost a perfect day out".into()); + post.tags.replace_all(tags); + // converting the field from append to replace would delete associations not in this list + let post = post.save(db).await?; + + { + info!("get back the post and tags"); + let post = post::Entity::load() + .filter_by_id(post_id) + .with(tag::Entity) + .one(db) + .await? + .unwrap(); + assert_eq!(post.tags.len(), 2); + assert_eq!(post.title, "Almost a perfect day out"); + assert_eq!(post.tags[0].tag, "foodie"); + assert_eq!(post.tags[1].tag, "downtown"); + } + + // only the association between post and tag is removed, + // but the tag itself is not deleted + info!("check that the tag sunny still exists"); + assert!(tag::Entity::find_by_tag("sunny").one(db).await?.is_some()); + + info!("cascade delete post, remove tag associations"); + post.delete(db).await?; + + ///// Part 3: self-referencing relations ///// + + info!("save a new user with a new profile"); + let sam = user::ActiveModel::builder() + .set_name("Sam") + .set_email("sam@rustacean.net") + .set_profile(profile::ActiveModel::builder().set_picture("Crab.jpg")) + .save(db) + .await?; + + // we can do it from left to right: user <- follower + info!("Add follower to Alice"); + let alice = alice.into_active_model().add_follower(bob).save(db).await?; + + // we can also do it in reverse: user -> following + info!("Sam starts following Alice"); + sam.add_following(alice).save(db).await?; + + info!("Query Alice's profile and followers"); + let alice = user::Entity::load() + .filter_by_email("alice@rust-lang.org") + .with(profile::Entity) + .with(user_follower::Entity) + .with(user_follower::Entity::REVERSE) + .one(db) + .await? + .unwrap(); + + assert_eq!(alice.name, "Alice"); + assert_eq!(alice.profile.as_ref().unwrap().picture, "Park"); + assert_eq!(alice.followers.len(), 2); + assert_eq!(alice.followers[0].name, "Bob"); + assert_eq!(alice.followers[1].name, "Sam"); + + Ok(()) +} diff --git a/examples/react_admin/README.md b/examples/react_admin/README.md new file mode 100644 index 0000000000..5115dc3314 --- /dev/null +++ b/examples/react_admin/README.md @@ -0,0 +1,11 @@ +# GraphQL based Admin Dashboard with Loco and Seaography + +In this tutorial, we would develop a GraphQL based admin dashboard with [Seaography](https://github.com/SeaQL/seaography) and Loco. + +Read The full tutorial [here](https://www.sea-ql.org/blog/2024-08-08-graphql-admin-dashboard-with-loco-seaography/). + +Read our first and second tutorial of the series, [Getting Started with Loco & SeaORM](https://www.sea-ql.org/blog/2024-05-28-getting-started-with-loco-seaorm/), [Adding GraphQL Support to Loco with Seaography](https://www.sea-ql.org/blog/2024-07-01-graphql-support-with-loco-seaography/), if you haven't. + +![Screenshot List](Screenshot-List.png) + +![Screenshot View](Screenshot-View.png) diff --git a/examples/react_admin/Screenshot-List.png b/examples/react_admin/Screenshot-List.png new file mode 100644 index 0000000000..b7d742451e Binary files /dev/null and b/examples/react_admin/Screenshot-List.png differ diff --git a/examples/react_admin/Screenshot-View.png b/examples/react_admin/Screenshot-View.png new file mode 100644 index 0000000000..0c13ddbab4 Binary files /dev/null and b/examples/react_admin/Screenshot-View.png differ diff --git a/examples/react_admin/backend/.cargo/config.toml b/examples/react_admin/backend/.cargo/config.toml new file mode 100644 index 0000000000..2def59ad76 --- /dev/null +++ b/examples/react_admin/backend/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +loco = "run --" +playground = "run --example playground" diff --git a/examples/react_admin/backend/.devcontainer/.env b/examples/react_admin/backend/.devcontainer/.env new file mode 100644 index 0000000000..1a3ca5b3c1 --- /dev/null +++ b/examples/react_admin/backend/.devcontainer/.env @@ -0,0 +1,6 @@ +POSTGRES_DB=loco_app +POSTGRES_USER=loco +POSTGRES_PASSWORD=loco +DATABASE_URL=postgres://loco:loco@db:5432/loco_app +REDIS_URL=redis://redis:6379 +MAILER_HOST=mailer \ No newline at end of file diff --git a/examples/react_admin/backend/.devcontainer/Dockerfile b/examples/react_admin/backend/.devcontainer/Dockerfile new file mode 100644 index 0000000000..49616a5f86 --- /dev/null +++ b/examples/react_admin/backend/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/devcontainers/rust:1 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends postgresql-client \ + && cargo install sea-orm-cli cargo-insta \ + && chown -R vscode /usr/local/cargo + +COPY .env /.env diff --git a/examples/react_admin/backend/.devcontainer/devcontainer.json b/examples/react_admin/backend/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ddc39eea3c --- /dev/null +++ b/examples/react_admin/backend/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "Loco", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "forwardPorts": [ + 3000 + ] +} \ No newline at end of file diff --git a/examples/react_admin/backend/.devcontainer/docker-compose.yml b/examples/react_admin/backend/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..6688eb2828 --- /dev/null +++ b/examples/react_admin/backend/.devcontainer/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3" + +services: + app: + build: + context: . + dockerfile: Dockerfile + command: sleep infinity + networks: + - db + - redis + - mailer + volumes: + - ../..:/workspaces:cached + env_file: + - .env + db: + image: postgres:15.3-alpine + restart: unless-stopped + ports: + - 5432:5432 + networks: + - db + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env + redis: + image: redis:latest + restart: unless-stopped + ports: + - 6379:6379 + networks: + - redis + mailer: + image: mailtutan/mailtutan:latest + restart: unless-stopped + ports: + - 1080:1080 + - 1025:1025 + networks: + - mailer + +volumes: + postgres-data: + +networks: + db: + redis: + mailer: diff --git a/examples/react_admin/backend/.github/workflows/ci.yaml b/examples/react_admin/backend/.github/workflows/ci.yaml new file mode 100644 index 0000000000..cb4763c9e4 --- /dev/null +++ b/examples/react_admin/backend/.github/workflows/ci.yaml @@ -0,0 +1,107 @@ +name: CI +on: + push: + branches: + - master + - main + pull_request: + +env: + RUST_TOOLCHAIN: stable + TOOLCHAIN_PROFILE: minimal + +jobs: + rustfmt: + name: Check Style + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + components: rustfmt + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Run Clippy + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms + + test: + name: Run Tests + runs-on: ubuntu-latest + + permissions: + contents: read + + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - "6379:6379" + postgres: + image: postgres + env: + POSTGRES_DB: postgres_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + # Set health checks to wait until postgres has started + options: --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --all + env: + REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}} + DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test diff --git a/examples/react_admin/backend/.gitignore b/examples/react_admin/backend/.gitignore new file mode 100644 index 0000000000..fb3bd4bbe1 --- /dev/null +++ b/examples/react_admin/backend/.gitignore @@ -0,0 +1,19 @@ +**/config/local.yaml +**/config/*.local.yaml +**/config/production.yaml + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# include cargo lock +!Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +uploads diff --git a/examples/react_admin/backend/.rustfmt.toml b/examples/react_admin/backend/.rustfmt.toml new file mode 100644 index 0000000000..26e84608fd --- /dev/null +++ b/examples/react_admin/backend/.rustfmt.toml @@ -0,0 +1,7 @@ +comment_width = 80 +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +max_width = 100 +use_small_heuristics = "Default" +wrap_comments = true diff --git a/examples/react_admin/backend/Cargo.lock b/examples/react_admin/backend/Cargo.lock new file mode 100644 index 0000000000..b356cf1113 --- /dev/null +++ b/examples/react_admin/backend/Cargo.lock @@ -0,0 +1,6117 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", + "tokio", +] + +[[package]] +name = "async-graphql" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "fast_chemail", + "fnv", + "futures-channel", + "futures-timer", + "futures-util", + "handlebars", + "http", + "indexmap", + "lru", + "mime", + "multer", + "num-traits", + "pin-project-lite", + "regex", + "rust_decimal", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-axum" +version = "7.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0344482a065904d2595377810e0bb35ad445ca4712b459757436303251d81b65" +dependencies = [ + "async-graphql", + "axum", + "bytes", + "futures-util", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", +] + +[[package]] +name = "async-graphql-derive" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum 0.26.3", + "syn 2.0.98", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-parser" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" +dependencies = [ + "bytes", + "indexmap", + "serde", + "serde_json", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "axum-macros", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "axum-test" +version = "17.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317c1f4ecc1e68e0ad5decb78478421055c963ce215e736ed97463fa609cd196" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum", + "bytes", + "bytesize", + "cookie", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "backon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "backtrace_printer" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d28de81c708c843640982b66573df0f0168d87e42854b563971f326745aab7" +dependencies = [ + "btparse-stable", + "colored 2.2.0", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bigdecimal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "btparse-stable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d75b8252ed252f881d1dc4482ae3c3854df6ee8183c1906bac50ff358f4f89f" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byte-unit" +version = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +dependencies = [ + "serde", +] + +[[package]] +name = "bytesize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" + +[[package]] +name = "cc" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom", + "once_cell", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cruet" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + +[[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 = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.98", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "duct" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] + +[[package]] +name = "duct_sh" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8139179d1d133ab7153920ba3812915b17c61e2514a6f98b1fd03f2c07668d1" +dependencies = [ + "duct", +] + +[[package]] +name = "ego-tree" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" +dependencies = [ + "base64 0.22.1", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "english-to-cron" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16423ac933fee80f05a52b435a912d5b08edbbbfe936e0042ebb3accdf303da" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[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 = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generator" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +dependencies = [ + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.58.0", +] + +[[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 = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2 0.5.8", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "insta" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86" +dependencies = [ + "console", + "linked-hash-map", + "once_cell", + "pest", + "pest_derive", + "pin-project", + "regex", + "serde", + "similar", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lettre" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e882e1489810a45919477602194312b1a7df0e5acc30a6188be7b520268f63f8" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "nom", + "percent-encoding", + "quoted_printable", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "socket2 0.5.8", + "tokio", + "tokio-rustls", + "url", + "webpki-roots", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "loco-gen" +version = "0.16.1" +source = "git+https://github.com/SeaQL/loco?branch=master#a5637bc645c5290597335b8f5032525ef9c727c7" +dependencies = [ + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.14.0", + "duct", + "heck 0.4.1", + "include_dir", + "regex", + "rrgen", + "serde", + "serde_json", + "tera", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "loco-rs" +version = "0.16.1" +source = "git+https://github.com/SeaQL/loco?branch=master#a5637bc645c5290597335b8f5032525ef9c727c7" +dependencies = [ + "argon2", + "async-trait", + "axum", + "axum-extra", + "axum-test", + "backtrace_printer", + "byte-unit", + "bytes", + "chrono", + "clap", + "colored 3.0.0", + "cruet 0.13.3", + "dashmap 6.1.0", + "duct", + "duct_sh", + "english-to-cron", + "futures-util", + "heck 0.4.1", + "include_dir", + "ipnetwork", + "jsonwebtoken", + "lettre", + "loco-gen", + "moka", + "opendal", + "rand 0.9.2", + "redis", + "regex", + "scraper", + "sea-orm", + "sea-orm-migration", + "semver", + "serde", + "serde_json", + "serde_variant", + "serde_yaml", + "sqlx", + "tera", + "thiserror 1.0.69", + "tokio", + "tokio-cron-scheduler", + "tokio-util", + "toml", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tree-fs", + "ulid", + "uuid", + "validator", +] + +[[package]] +name = "loco_seaography" +version = "0.1.0" +dependencies = [ + "async-graphql", + "async-graphql-axum", + "async-trait", + "axum", + "chrono", + "eyre", + "include_dir", + "insta", + "lazy_static", + "loco-rs", + "migration", + "rstest", + "sea-orm", + "seaography", + "serde", + "serde_json", + "serial_test", + "tokio", + "tokio-util", + "tower-service", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[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 = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "async-std", + "loco-rs", + "sea-orm-migration", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +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.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "moka" +version = "0.12.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "loom", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[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-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opendal" +version = "0.50.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb28bb6c64e116ceaf8dd4e87099d3cfea4a58e85e62b104fef74c91afba0f44" +dependencies = [ + "anyhow", + "async-trait", + "backon", + "base64 0.22.1", + "bytes", + "chrono", + "flagset", + "futures", + "getrandom 0.2.15", + "http", + "log", + "md-5", + "once_cell", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.11", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +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.98", + "version_check", + "yansi", +] + +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", +] + +[[package]] +name = "redis" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.8", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.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.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.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.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "reserve-port" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359fc315ed556eb0e42ce74e76f4b1cd807b50fa6307f3de4e51f92dbe86e2d5" +dependencies = [ + "lazy_static", + "thiserror 2.0.11", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rrgen" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7a7ede035354391a37e42aa4935b3d8921f0ded896d2ce44bb1a3b6dd76bab" +dependencies = [ + "cruet 0.13.3", + "fs-err", + "glob", + "heck 0.4.1", + "regex", + "serde", + "serde_json", + "serde_regex", + "serde_yaml", + "tera", + "thiserror 1.0.69", +] + +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.98", + "unicode-ident", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4bb9e7c9abe5fa5f30c2d8f8fefb9e0080a2c1e3c2e567318d2907054b35d3" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "mime_guess", + "rand 0.9.2", + "thiserror 2.0.11", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[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 = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" +dependencies = [ + "ahash 0.8.11", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "indexmap", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.1" +source = "git+https://github.com/SeaQL/sea-orm?branch=master#4d29a8a841fc17fe7ad19cfadf497f90113e2bc6" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures-util", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "serde", + "serde_json", + "sqlx", + "strum 0.27.2", + "thiserror 2.0.11", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "2.0.0-rc.1" +source = "git+https://github.com/SeaQL/sea-orm?branch=master#4d29a8a841fc17fe7ad19cfadf497f90113e2bc6" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "sea-schema", + "sqlx", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.1" +source = "git+https://github.com/SeaQL/sea-orm?branch=master#4d29a8a841fc17fe7ad19cfadf497f90113e2bc6" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.98", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "2.0.0-rc.1" +source = "git+https://github.com/SeaQL/sea-orm?branch=master#4d29a8a841fc17fe7ad19cfadf497f90113e2bc6" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a95b5b42aa664f4ee19707b63194ba28f82165bf52468450968d530e5c462c" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.8.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04db61539fefbddb68409949d73d51f73e62eb7306fe1d8478a867ff3be8c9dd" +dependencies = [ + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd199905d6c50267689c4625ee390b365b4bbac2230e8f180767c6511a13d7f" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.98", + "thiserror 2.0.11", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5eb19495858d8ae3663387a4f5298516c6f0171a7ca5681055450f190236b8" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ff014f61e9db0aeb1ce1fd7c3be630e4d3d87bc9ba37beaee8c899d14cb3ab8" +dependencies = [ + "futures", + "sea-query", + "sea-query-binder", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "seaography" +version = "2.0.0-rc.1" +source = "git+https://github.com/SeaQL/seaography.git?branch=main#82c023a607fd235bfb822b2cf48a790caea618ed" +dependencies = [ + "async-graphql", + "fnv", + "heck 0.4.1", + "itertools", + "lazy_static", + "sea-orm", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +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 = "serde_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap 5.5.3", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "servo_arc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.11", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.0", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.2", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.11", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.98", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.98", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.11", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.11", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.11", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + +[[package]] +name = "string_cache" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244292f3441c89febe5b5bdfbb6863aeaf4f64da810ea3050fd927b27b8d92ce" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.98", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +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.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-cron-scheduler" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2594dd7c2abbbafbb1c78d167fd10860dc7bd75f814cb051a1e0d3e796b9702" +dependencies = [ + "chrono", + "cron", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +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 = "tree-fs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6115680fa5fdb99b4ff19c9c3217e75116d2bb0eae82458c4e1818be6a10c7" +dependencies = [ + "rand 0.9.2", + "serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 2.0.11", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f294bff79170ed1c5633812aff1e565c35d993a36e757f9bc0accf5eec4e6045" +dependencies = [ + "rand 0.8.5", + "web-time", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom 0.2.15", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "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 = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.98", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[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.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/react_admin/backend/Cargo.toml b/examples/react_admin/backend/Cargo.toml new file mode 100644 index 0000000000..33c929ab3d --- /dev/null +++ b/examples/react_admin/backend/Cargo.toml @@ -0,0 +1,67 @@ +[workspace] + +[package] +edition = "2024" +name = "loco_seaography" +rust-version = "1.85.0" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +loco-rs = { version = "0.16" } +migration = { path = "migration" } + +async-trait = "0.1.74" +axum = { version = "0.8", features = ["multipart"] } +chrono = "0.4" +eyre = "0.6" +include_dir = "0.7" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.33.0", default-features = false } +tokio-util = "0.7.11" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } +uuid = { version = "1.6.0", features = ["v4"] } +validator = { version = "0.20" } + +async-graphql = { version = "7.0.17", features = [ + "decimal", + "chrono", + "dataloader", + "dynamic-schema", +] } +async-graphql-axum = { version = "7.0" } +lazy_static = { version = "1.4" } +tower-service = { version = "0.3" } + +[dependencies.sea-orm] +features = [ + "sqlx-sqlite", + "sqlx-postgres", + "runtime-tokio-rustls", + "macros", +] +version = "~2.0.0-rc.37" # sea-orm version + +[dependencies.seaography] +branch = "main" +features = ["with-decimal", "with-chrono"] +git = "https://github.com/SeaQL/seaography.git" +version = "~2.0.0-rc.1" # seaography version + +[[bin]] +name = "loco_seaography-cli" +path = "src/bin/main.rs" +required-features = [] + +[dev-dependencies] +insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } +loco-rs = { version = "0.16", features = ["testing"] } +rstest = "0.18.2" +serial_test = "2.0.0" + +[patch.crates-io] +loco-rs = { git = "https://github.com/SeaQL/loco", branch = "master" } diff --git a/examples/react_admin/backend/config/development.yaml b/examples/react_admin/backend/config/development.yaml new file mode 100644 index 0000000000..d7caa89f5c --- /dev/null +++ b/examples/react_admin/backend/config/development.yaml @@ -0,0 +1,129 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: true + # Enable pretty backtrace (sets RUST_BACKTRACE=1) + pretty_backtrace: true + # Log level, options: trace, debug, info, warn or error. + level: debug + # Define the logging format. options: compact, pretty or Json + format: compact + # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 3000 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Enable Etag cache header middleware + etag: + enable: true + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: false + # Duration time in milliseconds. + timeout: 5000 + cors: + enable: true + # Set the value of the [`Access-Control-Allow-Origin`][mdn] header + # allow_origins: + # - https://loco.rs + # Set the value of the [`Access-Control-Allow-Headers`][mdn] header + # allow_headers: + # - Content-Type + # Set the value of the [`Access-Control-Allow-Methods`][mdn] header + # allow_methods: + # - POST + # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds + # max_age: 3600 + +# Worker Configuration +workers: + # specifies the worker mode. Options: + # - BackgroundQueue - Workers operate asynchronously in the background, processing queued. + # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed. + # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities. + mode: BackgroundQueue + +# Mailer Configuration. +mailer: + # SMTP mailer configuration. + smtp: + # Enable/Disable smtp mailer. + enable: true + # SMTP server host. e.x localhost, smtp.gmail.com + host: {{ get_env(name="MAILER_HOST", default="localhost") }} + # SMTP server port + port: 1025 + # Use secure connection (SSL/TLS). + secure: false + # auth: + # user: + # password: + +# Initializers Configuration +# initializers: +# oauth2: +# authorization_code: # Authorization code grant type +# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config. +# ... other fields + +# Database Configuration +database: + # Database connection URI + uri: {{ get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/loco_seaography_development") }} + # When enabled, the sql query will be logged. + enable_logging: false + # Set the timeout duration when acquiring a connection. + connect_timeout: 500 + # Set the idle duration before closing a connection. + idle_timeout: 500 + # Minimum number of connections for a pool. + min_connections: 1 + # Maximum number of connections for a pool. + max_connections: 1 + # Run migration up when application loaded + auto_migrate: true + # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_truncate: false + # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_recreate: false + +# Redis Configuration +redis: + # Redis connection URI + uri: {{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }} + # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode + dangerously_flush: false + +# Authentication Configuration +auth: + # JWT authentication + jwt: + # Secret key for token generation and verification + secret: pByQUgg4GmXKAqQQvAGo + # Token expiration time in seconds + expiration: 604800 # 7 days + diff --git a/examples/react_admin/backend/examples/playground.rs b/examples/react_admin/backend/examples/playground.rs new file mode 100644 index 0000000000..dfde4e0340 --- /dev/null +++ b/examples/react_admin/backend/examples/playground.rs @@ -0,0 +1,22 @@ +use eyre::Context; +#[allow(unused_imports)] +use loco_rs::{cli::playground, prelude::*}; +use loco_seaography::app::App; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let _ctx = playground::().await.context("playground")?; + + // let active_model: articles::ActiveModel = ActiveModel { + // title: Set(Some("how to build apps in 3 steps".to_string())), + // content: Set(Some("use Loco: https://loco.rs".to_string())), + // ..Default::default() + // }; + // active_model.insert(&ctx.db).await.unwrap(); + + // let res = articles::Entity::find().all(&ctx.db).await.unwrap(); + // println!("{:?}", res); + println!("welcome to playground. edit me at `examples/playground.rs`"); + + Ok(()) +} diff --git a/examples/react_admin/backend/migration/Cargo.toml b/examples/react_admin/backend/migration/Cargo.toml new file mode 100644 index 0000000000..3729f89a98 --- /dev/null +++ b/examples/react_admin/backend/migration/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +loco-rs = { version = "0.16" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dependencies.sea-orm-migration] +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature +] +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/react_admin/backend/migration/README.md b/examples/react_admin/backend/migration/README.md new file mode 100644 index 0000000000..b3ea53eb44 --- /dev/null +++ b/examples/react_admin/backend/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- migrate generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/react_admin/backend/migration/src/lib.rs b/examples/react_admin/backend/migration/src/lib.rs new file mode 100644 index 0000000000..f4f27b76b3 --- /dev/null +++ b/examples/react_admin/backend/migration/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(elided_lifetimes_in_paths)] +#![allow(clippy::wildcard_imports)] +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_users; +mod m20231103_114510_notes; +mod m20240520_173001_files; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20220101_000001_users::Migration), + Box::new(m20231103_114510_notes::Migration), + Box::new(m20240520_173001_files::Migration), + ] + } +} diff --git a/examples/react_admin/backend/migration/src/m20220101_000001_users.rs b/examples/react_admin/backend/migration/src/m20220101_000001_users.rs new file mode 100644 index 0000000000..fa7fb7717b --- /dev/null +++ b/examples/react_admin/backend/migration/src/m20220101_000001_users.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let table = table_auto("users") + .col(pk_auto("id")) + .col(uuid("pid")) + .col(string_uniq("email")) + .col(string("password")) + .col(string("api_key").unique_key()) + .col(string("name")) + .col(string_null("reset_token")) + .col(timestamp_null("reset_sent_at")) + .col(string_null("email_verification_token")) + .col(timestamp_null("email_verification_sent_at")) + .col(timestamp_null("email_verified_at")) + .to_owned(); + manager.create_table(table).await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("users").to_owned()) + .await + } +} diff --git a/examples/react_admin/backend/migration/src/m20231103_114510_notes.rs b/examples/react_admin/backend/migration/src/m20231103_114510_notes.rs new file mode 100644 index 0000000000..04f13f4867 --- /dev/null +++ b/examples/react_admin/backend/migration/src/m20231103_114510_notes.rs @@ -0,0 +1,25 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("notes") + .col(pk_auto("id")) + .col(string_null("title")) + .col(string_null("content")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("notes").to_owned()) + .await + } +} diff --git a/examples/react_admin/backend/migration/src/m20240520_173001_files.rs b/examples/react_admin/backend/migration/src/m20240520_173001_files.rs new file mode 100644 index 0000000000..3162742bdb --- /dev/null +++ b/examples/react_admin/backend/migration/src/m20240520_173001_files.rs @@ -0,0 +1,31 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto("files") + .col(pk_auto("id")) + .col(integer("notes_id")) + .col(string("file_path")) + .foreign_key( + ForeignKey::create() + .name("FK_files_notes_id") + .from("files", "notes_id") + .to("notes", "id"), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("files").to_owned()) + .await + } +} diff --git a/examples/react_admin/backend/migration/src/main.rs b/examples/react_admin/backend/migration/src/main.rs new file mode 100644 index 0000000000..f054deaf88 --- /dev/null +++ b/examples/react_admin/backend/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[tokio::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/examples/react_admin/backend/src/app.rs b/examples/react_admin/backend/src/app.rs new file mode 100644 index 0000000000..b816a4d7f1 --- /dev/null +++ b/examples/react_admin/backend/src/app.rs @@ -0,0 +1,81 @@ +use std::path::Path; + +use async_trait::async_trait; +use loco_rs::{ + Result, + app::{AppContext, Hooks}, + bgworker::{BackgroundWorker, Queue}, + boot::{BootResult, StartMode, create_app}, + config::Config, + controller::AppRoutes, + db::{self, truncate_table}, + environment::Environment, + task::Tasks, +}; +use migration::Migrator; + +use crate::{ + controllers, + models::_entities::{notes, users}, + tasks, + workers::downloader::DownloadWorker, +}; + +pub struct App; +#[async_trait] +impl Hooks for App { + fn app_name() -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn app_version() -> String { + format!( + "{} ({})", + env!("CARGO_PKG_VERSION"), + option_env!("BUILD_SHA") + .or(option_env!("GITHUB_SHA")) + .unwrap_or("dev") + ) + } + + async fn boot( + mode: StartMode, + environment: &Environment, + config: Config, + ) -> Result { + create_app::(mode, environment, config).await + } + + fn routes(_ctx: &AppContext) -> AppRoutes { + AppRoutes::with_default_routes() + .prefix("/api") + .add_route(controllers::notes::routes()) + .add_route(controllers::auth::routes()) + .add_route(controllers::user::routes()) + .add_route(controllers::files::routes()) + .add_route(controllers::graphql::routes()) + } + + async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { + queue.register(DownloadWorker::build(ctx)).await?; + Ok(()) + } + + fn register_tasks(tasks: &mut Tasks) { + tasks.register(tasks::seed::SeedData); + } + + async fn truncate(ctx: &AppContext) -> Result<()> { + let db = &ctx.db; + truncate_table(db, users::Entity).await?; + truncate_table(db, notes::Entity).await?; + Ok(()) + } + + async fn seed(ctx: &AppContext, base: &Path) -> Result<()> { + let db = &ctx.db; + db::seed::(db, &base.join("users.yaml").display().to_string()).await?; + db::seed::(db, &base.join("notes.yaml").display().to_string()).await?; + Ok(()) + } +} diff --git a/examples/react_admin/backend/src/bin/main.rs b/examples/react_admin/backend/src/bin/main.rs new file mode 100644 index 0000000000..226711dcf0 --- /dev/null +++ b/examples/react_admin/backend/src/bin/main.rs @@ -0,0 +1,9 @@ +use loco_rs::cli; +use loco_seaography::app::App; +use migration::Migrator; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + cli::main::().await?; + Ok(()) +} diff --git a/examples/react_admin/backend/src/controllers/auth.rs b/examples/react_admin/backend/src/controllers/auth.rs new file mode 100644 index 0000000000..fd15dcdce3 --- /dev/null +++ b/examples/react_admin/backend/src/controllers/auth.rs @@ -0,0 +1,156 @@ +use axum::debug_handler; +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::{ + mailers::auth::AuthMailer, + models::{ + _entities::users, + users::{LoginParams, RegisterParams}, + }, + views::auth::LoginResponse, +}; +#[derive(Debug, Deserialize, Serialize)] +pub struct VerifyParams { + pub token: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ForgotParams { + pub email: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ResetParams { + pub token: String, + pub password: String, +} + +/// Register function creates a new user with the given parameters and sends a +/// welcome email to the user +#[debug_handler] +async fn register( + State(ctx): State, + Json(params): Json, +) -> Result { + let res = users::Model::create_with_password(&ctx.db, ¶ms).await; + + let user = match res { + Ok(user) => user, + Err(err) => { + tracing::info!( + message = err.to_string(), + user_email = ¶ms.email, + "could not register user", + ); + return format::json(()); + } + }; + + // Skip email verification, all new registrations are considered verified + let _user = user.into_active_model().verified(&ctx.db).await?; + + // Skip sending verification email as we don't have a mail server + /* + let user = user + .into_active_model() + .set_email_verification_sent(&ctx.db) + .await?; + + AuthMailer::send_welcome(&ctx, &user).await?; + */ + + format::json(()) +} + +/// Verify register user. if the user not verified his email, he can't login to +/// the system. +#[debug_handler] +async fn verify( + State(ctx): State, + Json(params): Json, +) -> Result { + let user = users::Model::find_by_verification_token(&ctx.db, ¶ms.token).await?; + + if user.email_verified_at.is_some() { + tracing::info!(pid = user.pid.to_string(), "user already verified"); + } else { + let active_model = user.into_active_model(); + let user = active_model.verified(&ctx.db).await?; + tracing::info!(pid = user.pid.to_string(), "user verified"); + } + + format::json(()) +} + +/// In case the user forgot his password this endpoints generate a forgot token +/// and send email to the user. In case the email not found in our DB, we are +/// returning a valid request for for security reasons (not exposing users DB +/// list). +#[debug_handler] +async fn forgot( + State(ctx): State, + Json(params): Json, +) -> Result { + let Ok(user) = users::Model::find_by_email(&ctx.db, ¶ms.email).await else { + // we don't want to expose our users email. if the email is invalid we still + // returning success to the caller + return format::json(()); + }; + + let user = user + .into_active_model() + .set_forgot_password_sent(&ctx.db) + .await?; + + AuthMailer::forgot_password(&ctx, &user).await?; + + format::json(()) +} + +/// reset user password by the given parameters +#[debug_handler] +async fn reset(State(ctx): State, Json(params): Json) -> Result { + let Ok(user) = users::Model::find_by_reset_token(&ctx.db, ¶ms.token).await else { + // we don't want to expose our users email. if the email is invalid we still + // returning success to the caller + tracing::info!("reset token not found"); + + return format::json(()); + }; + user.into_active_model() + .reset_password(&ctx.db, ¶ms.password) + .await?; + + format::json(()) +} + +/// Creates a user login and returns a token +#[debug_handler] +async fn login(State(ctx): State, Json(params): Json) -> Result { + let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?; + + let valid = user.verify_password(¶ms.password); + + if !valid { + return unauthorized("unauthorized!"); + } + + let jwt_secret = ctx.config.get_jwt_config()?; + + let token = user + .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration) + .or_else(|_| unauthorized("unauthorized!"))?; + + format::json(LoginResponse::new(&user, &token)) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("auth") + .add("/register", post(register)) + .add("/verify", post(verify)) + .add("/login", post(login)) + .add("/forgot", post(forgot)) + .add("/reset", post(reset)) +} diff --git a/examples/react_admin/backend/src/controllers/files.rs b/examples/react_admin/backend/src/controllers/files.rs new file mode 100644 index 0000000000..4d76da874d --- /dev/null +++ b/examples/react_admin/backend/src/controllers/files.rs @@ -0,0 +1,126 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use std::path::PathBuf; + +use axum::{body::Body, debug_handler, extract::Multipart}; +use loco_rs::prelude::*; +use sea_orm::QueryOrder; +use tokio::{fs, io::AsyncWriteExt}; +use tokio_util::io::ReaderStream; + +use crate::models::_entities::files; + +const UPLOAD_DIR: &str = "./uploads"; + +#[debug_handler] +pub async fn upload( + _auth: auth::JWT, + Path(notes_id): Path, + State(ctx): State, + mut multipart: Multipart, +) -> Result { + // Collect all uploaded files + let mut files = Vec::new(); + + // Iterate all files in the POST body + while let Some(field) = multipart.next_field().await.map_err(|err| { + tracing::error!(error = ?err,"could not readd multipart"); + Error::BadRequest("could not readd multipart".into()) + })? { + // Get the file name + let file_name = match field.file_name() { + Some(file_name) => file_name.to_string(), + _ => return Err(Error::BadRequest("file name not found".into())), + }; + + // Get the file content as bytes + let content = field.bytes().await.map_err(|err| { + tracing::error!(error = ?err,"could not readd bytes"); + Error::BadRequest("could not readd bytes".into()) + })?; + + // Create a folder to store the uploaded file + let now = chrono::offset::Local::now() + .format("%Y%m%d_%H%M%S") + .to_string(); + let uuid = uuid::Uuid::new_v4().to_string(); + let folder = format!("{now}_{uuid}"); + let upload_folder = PathBuf::from(UPLOAD_DIR).join(&folder); + fs::create_dir_all(&upload_folder).await?; + + // Write the file into the newly created folder + let path = upload_folder.join(file_name); + let mut f = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&path) + .await?; + f.write_all(&content).await?; + f.flush().await?; + + // Record the file upload in database + let file = files::ActiveModel { + notes_id: ActiveValue::Set(notes_id), + file_path: ActiveValue::Set( + path.strip_prefix(UPLOAD_DIR) + .unwrap() + .to_str() + .unwrap() + .to_string(), + ), + ..Default::default() + } + .insert(&ctx.db) + .await?; + + files.push(file); + } + + format::json(files) +} + +#[debug_handler] +pub async fn list( + _auth: auth::JWT, + Path(notes_id): Path, + State(ctx): State, +) -> Result { + // Fetch all files uploaded for a specific notes + let files = files::Entity::find() + .filter(files::Column::NotesId.eq(notes_id)) + .order_by_asc(files::Column::Id) + .all(&ctx.db) + .await?; + + format::json(files) +} + +#[debug_handler] +pub async fn view( + _auth: auth::JWT, + Path(files_id): Path, + State(ctx): State, +) -> Result { + // Fetch the file info from database + let file = files::Entity::find_by_id(files_id) + .one(&ctx.db) + .await? + .expect("File not found"); + + // Stream the file + let file = fs::File::open(format!("{UPLOAD_DIR}/{}", file.file_path)).await?; + let stream = ReaderStream::new(file); + let body = Body::from_stream(stream); + + Ok(format::render().response().body(body)?) +} + +pub fn routes() -> Routes { + // Bind the routes + Routes::new() + .prefix("files") + .add("/upload/{notes_id}", post(upload)) + .add("/list/{notes_id}", get(list)) + .add("/view/{files_id}", get(view)) +} diff --git a/examples/react_admin/backend/src/controllers/graphql.rs b/examples/react_admin/backend/src/controllers/graphql.rs new file mode 100644 index 0000000000..28aeebef92 --- /dev/null +++ b/examples/react_admin/backend/src/controllers/graphql.rs @@ -0,0 +1,39 @@ +use async_graphql::http::{GraphQLPlaygroundConfig, playground_source}; +use axum::{body::Body, extract::Request}; +use loco_rs::prelude::*; +use tower_service::Service; + +use crate::graphql::query_root; + +// GraphQL playground UI +async fn graphql_playground() -> Result { + // The `GraphQLPlaygroundConfig` take one parameter + // which is the URL of the GraphQL handler: `/api/graphql` + let res = playground_source(GraphQLPlaygroundConfig::new("/api/graphql")); + + Ok(Response::new(res.into())) +} + +async fn graphql_handler(State(ctx): State, req: Request) -> Result { + const DEPTH: usize = 1_000; + const COMPLEXITY: usize = 1_000; + // Construct the the GraphQL query root + let schema = query_root::schema(ctx.db.clone(), DEPTH, COMPLEXITY).unwrap(); + // GraphQL handler + let mut graphql_handler = async_graphql_axum::GraphQL::new(schema); + // Execute GraphQL request and fetch the results + let res = graphql_handler.call(req).await.unwrap(); + + Ok(res) +} + +pub fn routes() -> Routes { + // Define route + Routes::new() + // We put all GraphQL route behind `graphql` prefix + .prefix("graphql") + // GraphQL playground page is a GET request + .add("/", get(graphql_playground)) + // GraphQL handler is a POST request + .add("/", post(graphql_handler)) +} diff --git a/examples/react_admin/backend/src/controllers/mod.rs b/examples/react_admin/backend/src/controllers/mod.rs new file mode 100644 index 0000000000..659c34f1de --- /dev/null +++ b/examples/react_admin/backend/src/controllers/mod.rs @@ -0,0 +1,5 @@ +pub mod auth; +pub mod files; +pub mod graphql; +pub mod notes; +pub mod user; diff --git a/examples/react_admin/backend/src/controllers/notes.rs b/examples/react_admin/backend/src/controllers/notes.rs new file mode 100644 index 0000000000..3377158f9a --- /dev/null +++ b/examples/react_admin/backend/src/controllers/notes.rs @@ -0,0 +1,75 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use axum::debug_handler; +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::notes::{ActiveModel, Entity, Model}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + pub title: Option, + pub content: Option, +} + +impl Params { + fn update(&self, item: &mut ActiveModel) { + item.title = Set(self.title.clone()); + item.content = Set(self.content.clone()); + } +} + +async fn load_item(ctx: &AppContext, id: i32) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +pub async fn list(State(ctx): State) -> Result { + format::json(Entity::find().all(&ctx.db).await?) +} + +#[debug_handler] +pub async fn add(State(ctx): State, Json(params): Json) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + let item = item.insert(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +pub async fn update( + Path(id): Path, + State(ctx): State, + Json(params): Json, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + let item = item.update(&ctx.db).await?; + format::json(item) +} + +#[debug_handler] +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +#[debug_handler] +pub async fn get_one(Path(id): Path, State(ctx): State) -> Result { + format::json(load_item(&ctx, id).await?) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("notes") + .add("/", get(list)) + .add("/", post(add)) + .add("/{id}", get(get_one)) + .add("/{id}", delete(remove)) + .add("/{id}", post(update)) +} diff --git a/examples/react_admin/backend/src/controllers/user.rs b/examples/react_admin/backend/src/controllers/user.rs new file mode 100644 index 0000000000..1f432ae9ef --- /dev/null +++ b/examples/react_admin/backend/src/controllers/user.rs @@ -0,0 +1,14 @@ +use axum::debug_handler; +use loco_rs::prelude::*; + +use crate::{models::_entities::users, views::user::CurrentResponse}; + +#[debug_handler] +async fn current(auth: auth::JWT, State(ctx): State) -> Result { + let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; + format::json(CurrentResponse::new(&user)) +} + +pub fn routes() -> Routes { + Routes::new().prefix("user").add("/current", get(current)) +} diff --git a/examples/react_admin/backend/src/fixtures/notes.yaml b/examples/react_admin/backend/src/fixtures/notes.yaml new file mode 100644 index 0000000000..2237af81c5 --- /dev/null +++ b/examples/react_admin/backend/src/fixtures/notes.yaml @@ -0,0 +1,11 @@ +--- +- id: 1 + title: Loco note 1 + content: Loco note 1 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + title: Loco note 2 + content: Loco note 2 content + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/react_admin/backend/src/fixtures/users.yaml b/examples/react_admin/backend/src/fixtures/users.yaml new file mode 100644 index 0000000000..8b414e4734 --- /dev/null +++ b/examples/react_admin/backend/src/fixtures/users.yaml @@ -0,0 +1,17 @@ +--- +- id: 1 + pid: 11111111-1111-1111-1111-111111111111 + email: user1@example.com + password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" + api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758 + name: user1 + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" +- id: 2 + pid: 22222222-2222-2222-2222-222222222222 + email: user2@example.com + password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" + api_key: lo-153561ca-fa84-4e1b-813a-c62526d0a77e + name: user2 + created_at: "2023-11-12T12:34:56.789" + updated_at: "2023-11-12T12:34:56.789" diff --git a/examples/react_admin/backend/src/graphql/mod.rs b/examples/react_admin/backend/src/graphql/mod.rs new file mode 100644 index 0000000000..5fffdf9887 --- /dev/null +++ b/examples/react_admin/backend/src/graphql/mod.rs @@ -0,0 +1 @@ +pub mod query_root; diff --git a/examples/react_admin/backend/src/graphql/query_root.rs b/examples/react_admin/backend/src/graphql/query_root.rs new file mode 100644 index 0000000000..ee144cf47f --- /dev/null +++ b/examples/react_admin/backend/src/graphql/query_root.rs @@ -0,0 +1,25 @@ +use async_graphql::dynamic::*; +use sea_orm::DatabaseConnection; +use seaography::{Builder, BuilderContext}; + +lazy_static::lazy_static! { static ref CONTEXT: BuilderContext = BuilderContext::default(); } + +pub fn schema( + database: DatabaseConnection, + depth: usize, + complexity: usize, +) -> Result { + // Builder of Seaography query root + let builder = Builder::new(&CONTEXT, database.clone()); + // Register SeaORM entities + let builder = crate::models::_entities::register_entity_modules(builder); + // Configure async GraphQL limits + let schema = builder + .schema_builder() + // The depth is the number of nesting levels of the field + .limit_depth(depth) + // The complexity is the number of fields in the query + .limit_complexity(complexity); + // Finish up with including SeaORM database connection + schema.data(database).finish() +} diff --git a/examples/react_admin/backend/src/lib.rs b/examples/react_admin/backend/src/lib.rs new file mode 100644 index 0000000000..12ba7f8841 --- /dev/null +++ b/examples/react_admin/backend/src/lib.rs @@ -0,0 +1,8 @@ +pub mod app; +pub mod controllers; +pub mod graphql; +pub mod mailers; +pub mod models; +pub mod tasks; +pub mod views; +pub mod workers; diff --git a/examples/react_admin/backend/src/mailers/auth.rs b/examples/react_admin/backend/src/mailers/auth.rs new file mode 100644 index 0000000000..30bb1bf2f5 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth.rs @@ -0,0 +1,65 @@ +// auth mailer +#![allow(non_upper_case_globals)] + +use loco_rs::prelude::*; +use serde_json::json; + +use crate::models::users; + +static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome"); +static forgot: Dir<'_> = include_dir!("src/mailers/auth/forgot"); +// #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets +// move on for now. + +#[allow(clippy::module_name_repetitions)] +pub struct AuthMailer {} +impl Mailer for AuthMailer {} +impl AuthMailer { + /// Sending welcome email the the given user + /// + /// # Errors + /// + /// When email sending is failed + pub async fn send_welcome(ctx: &AppContext, user: &users::Model) -> Result<()> { + Self::mail_template( + ctx, + &welcome, + mailer::Args { + to: user.email.to_string(), + locals: json!({ + "name": user.name, + "verifyToken": user.email_verification_token, + "domain": ctx.config.server.full_url() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } + + /// Sending forgot password email + /// + /// # Errors + /// + /// When email sending is failed + pub async fn forgot_password(ctx: &AppContext, user: &users::Model) -> Result<()> { + Self::mail_template( + ctx, + &forgot, + mailer::Args { + to: user.email.to_string(), + locals: json!({ + "name": user.name, + "resetToken": user.reset_token, + "domain": ctx.config.server.full_url() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } +} diff --git a/examples/react_admin/backend/src/mailers/auth/forgot/html.t b/examples/react_admin/backend/src/mailers/auth/forgot/html.t new file mode 100644 index 0000000000..221dd60205 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth/forgot/html.t @@ -0,0 +1,11 @@ +; + + + Hey {{name}}, + Forgot your password? No worries! You can reset it by clicking the link below: + Reset Your Password + If you didn't request a password reset, please ignore this email. + Best regards,
The Loco Team
+ + + diff --git a/examples/react_admin/backend/src/mailers/auth/forgot/subject.t b/examples/react_admin/backend/src/mailers/auth/forgot/subject.t new file mode 100644 index 0000000000..4938df1e30 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth/forgot/subject.t @@ -0,0 +1 @@ +Your reset password link diff --git a/examples/react_admin/backend/src/mailers/auth/forgot/text.t b/examples/react_admin/backend/src/mailers/auth/forgot/text.t new file mode 100644 index 0000000000..58c30fd8d1 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth/forgot/text.t @@ -0,0 +1,3 @@ +Reset your password with this link: + +http://localhost/reset#{{resetToken}} diff --git a/examples/react_admin/backend/src/mailers/auth/welcome/html.t b/examples/react_admin/backend/src/mailers/auth/welcome/html.t new file mode 100644 index 0000000000..ae4c41c654 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth/welcome/html.t @@ -0,0 +1,13 @@ +; + + + Dear {{name}}, + Welcome to Loco! You can now log in to your account. + Before you get started, please verify your account by clicking the link below: + + Verify Your Account + +

Best regards,
The Loco Team

+ + + diff --git a/examples/react_admin/backend/src/mailers/auth/welcome/subject.t b/examples/react_admin/backend/src/mailers/auth/welcome/subject.t new file mode 100644 index 0000000000..82cc6fbf7b --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth/welcome/subject.t @@ -0,0 +1 @@ +Welcome {{name}} diff --git a/examples/react_admin/backend/src/mailers/auth/welcome/text.t b/examples/react_admin/backend/src/mailers/auth/welcome/text.t new file mode 100644 index 0000000000..63beefd565 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/auth/welcome/text.t @@ -0,0 +1,4 @@ +Welcome {{name}}, you can now log in. + Verify your account with the link below: + + http://localhost/verify#{{verifyToken}} diff --git a/examples/react_admin/backend/src/mailers/mod.rs b/examples/react_admin/backend/src/mailers/mod.rs new file mode 100644 index 0000000000..0e4a05d597 --- /dev/null +++ b/examples/react_admin/backend/src/mailers/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/examples/react_admin/backend/src/models/_entities/files.rs b/examples/react_admin/backend/src/models/_entities/files.rs new file mode 100644 index 0000000000..186b0cb912 --- /dev/null +++ b/examples/react_admin/backend/src/models/_entities/files.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "files")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub notes_id: i32, + pub file_path: String, + #[sea_orm(belongs_to, from = "notes_id", to = "id")] + pub notes: HasOne, +} diff --git a/examples/react_admin/backend/src/models/_entities/mod.rs b/examples/react_admin/backend/src/models/_entities/mod.rs new file mode 100644 index 0000000000..23ec9cc0bd --- /dev/null +++ b/examples/react_admin/backend/src/models/_entities/mod.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5 + +pub mod prelude; + +pub mod files; +pub mod notes; +pub mod users; + +seaography::register_entity_modules!([files, notes, users]); diff --git a/examples/react_admin/backend/src/models/_entities/notes.rs b/examples/react_admin/backend/src/models/_entities/notes.rs new file mode 100644 index 0000000000..f3325af0c2 --- /dev/null +++ b/examples/react_admin/backend/src/models/_entities/notes.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.10 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "notes")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub title: Option, + pub content: Option, + #[sea_orm(has_many)] + pub files: HasMany, +} diff --git a/examples/react_admin/backend/src/models/_entities/prelude.rs b/examples/react_admin/backend/src/models/_entities/prelude.rs new file mode 100644 index 0000000000..109d94d772 --- /dev/null +++ b/examples/react_admin/backend/src/models/_entities/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5 + +pub use super::{files::Entity as Files, notes::Entity as Notes, users::Entity as Users}; diff --git a/examples/react_admin/backend/src/models/_entities/users.rs b/examples/react_admin/backend/src/models/_entities/users.rs new file mode 100644 index 0000000000..39930492ec --- /dev/null +++ b/examples/react_admin/backend/src/models/_entities/users.rs @@ -0,0 +1,31 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.5 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key)] + pub id: i32, + pub pid: Uuid, + #[sea_orm(unique)] + pub email: String, + pub password: String, + #[sea_orm(unique)] + pub api_key: String, + pub name: String, + pub reset_token: Option, + pub reset_sent_at: Option, + pub email_verification_token: Option, + pub email_verification_sent_at: Option, + pub email_verified_at: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] +pub enum RelatedEntity {} diff --git a/examples/react_admin/backend/src/models/files.rs b/examples/react_admin/backend/src/models/files.rs new file mode 100644 index 0000000000..6e043ce0a3 --- /dev/null +++ b/examples/react_admin/backend/src/models/files.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::files::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/react_admin/backend/src/models/mod.rs b/examples/react_admin/backend/src/models/mod.rs new file mode 100644 index 0000000000..882ae054a2 --- /dev/null +++ b/examples/react_admin/backend/src/models/mod.rs @@ -0,0 +1,4 @@ +pub mod _entities; +pub mod files; +pub mod notes; +pub mod users; diff --git a/examples/react_admin/backend/src/models/notes.rs b/examples/react_admin/backend/src/models/notes.rs new file mode 100644 index 0000000000..1102598236 --- /dev/null +++ b/examples/react_admin/backend/src/models/notes.rs @@ -0,0 +1,7 @@ +use sea_orm::entity::prelude::*; + +use super::_entities::notes::ActiveModel; + +impl ActiveModelBehavior for ActiveModel { + // extend activemodel below (keep comment for generators) +} diff --git a/examples/react_admin/backend/src/models/users.rs b/examples/react_admin/backend/src/models/users.rs new file mode 100644 index 0000000000..b303c554d6 --- /dev/null +++ b/examples/react_admin/backend/src/models/users.rs @@ -0,0 +1,271 @@ +use async_trait::async_trait; +use chrono::offset::Local; +use loco_rs::{auth::jwt, hash, prelude::*}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub use super::_entities::users::{self, ActiveModel, Entity, Model}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginParams { + pub email: String, + pub password: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RegisterParams { + pub email: String, + pub password: String, + pub name: String, +} + +#[derive(Debug, Validate, Deserialize)] +pub struct Validator { + #[validate(length(min = 2, message = "Name must be at least 2 characters long."))] + pub name: String, + #[validate(email(message = "invalid email"))] + pub email: String, +} + +impl Validatable for super::_entities::users::ActiveModel { + fn validator(&self) -> Box { + Box::new(Validator { + name: self.name.as_ref().to_owned(), + email: self.email.as_ref().to_owned(), + }) + } +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for super::_entities::users::ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> Result + where + C: ConnectionTrait, + { + self.validate()?; + if insert { + let mut this = self; + this.pid = ActiveValue::Set(Uuid::new_v4()); + this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4())); + Ok(this) + } else { + Ok(self) + } + } +} + +#[async_trait] +impl Authenticable for super::_entities::users::Model { + async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ApiKey.eq(api_key)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult { + Self::find_by_pid(db, claims_key).await + } +} + +impl super::_entities::users::Model { + /// finds a user by the provided email + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::Email.eq(email)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided verification token + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_verification_token( + db: &DatabaseConnection, + token: &str, + ) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::EmailVerificationToken.eq(token)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// /// finds a user by the provided reset token + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ResetToken.eq(token)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided pid + /// + /// # Errors + /// + /// When could not find user or DB query error + pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult { + let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?; + let user = users::Entity::find() + .filter(users::Column::Pid.eq(parse_uuid)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// finds a user by the provided api key + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult { + let user = users::Entity::find() + .filter(users::Column::ApiKey.eq(api_key)) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + + /// Verifies whether the provided plain password matches the hashed password + /// + /// # Errors + /// + /// when could not verify password + #[must_use] + pub fn verify_password(&self, password: &str) -> bool { + hash::verify_password(password, &self.password) + } + + /// Asynchronously creates a user with a password and saves it to the + /// database. + /// + /// # Errors + /// + /// When could not save the user into the DB + pub async fn create_with_password( + db: &DatabaseConnection, + params: &RegisterParams, + ) -> ModelResult { + let txn = db.begin().await?; + + if users::Entity::find() + .filter(users::Column::Email.eq(¶ms.email)) + .one(&txn) + .await? + .is_some() + { + return Err(ModelError::EntityAlreadyExists {}); + } + + let password_hash = + hash::hash_password(¶ms.password).map_err(|e| ModelError::Any(e.into()))?; + let user = users::ActiveModel { + email: ActiveValue::set(params.email.to_string()), + password: ActiveValue::set(password_hash), + name: ActiveValue::set(params.name.to_string()), + ..Default::default() + } + .insert(&txn) + .await?; + + txn.commit().await?; + + Ok(user) + } + + /// Creates a JWT + /// + /// # Errors + /// + /// when could not convert user claims to jwt token + pub fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult { + Ok(jwt::JWT::new(secret).generate_token( + *expiration, + self.pid.to_string(), + Default::default(), + )?) + } +} + +impl super::_entities::users::ActiveModel { + /// Sets the email verification information for the user and + /// updates it in the database. + /// + /// This method is used to record the timestamp when the email verification + /// was sent and generate a unique verification token for the user. + /// + /// # Errors + /// + /// when has DB query error + pub async fn set_email_verification_sent( + mut self, + db: &DatabaseConnection, + ) -> ModelResult { + self.email_verification_sent_at = ActiveValue::set(Some(Local::now().naive_local())); + self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); + Ok(self.update(db).await?) + } + + /// Sets the information for a reset password request, + /// generates a unique reset password token, and updates it in the + /// database. + /// + /// This method records the timestamp when the reset password token is sent + /// and generates a unique token for the user. + /// + /// # Arguments + /// + /// # Errors + /// + /// when has DB query error + pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult { + self.reset_sent_at = ActiveValue::set(Some(Local::now().naive_local())); + self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string())); + Ok(self.update(db).await?) + } + + /// Records the verification time when a user verifies their + /// email and updates it in the database. + /// + /// This method sets the timestamp when the user successfully verifies their + /// email. + /// + /// # Errors + /// + /// when has DB query error + pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult { + self.email_verified_at = ActiveValue::set(Some(Local::now().naive_local())); + Ok(self.update(db).await?) + } + + /// Resets the current user password with a new password and + /// updates it in the database. + /// + /// This method hashes the provided password and sets it as the new password + /// for the user. + /// # Errors + /// + /// when has DB query error or could not hashed the given password + pub async fn reset_password( + mut self, + db: &DatabaseConnection, + password: &str, + ) -> ModelResult { + self.password = + ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?); + Ok(self.update(db).await?) + } +} diff --git a/examples/react_admin/backend/src/tasks/mod.rs b/examples/react_admin/backend/src/tasks/mod.rs new file mode 100644 index 0000000000..01fbddaa29 --- /dev/null +++ b/examples/react_admin/backend/src/tasks/mod.rs @@ -0,0 +1 @@ +pub mod seed; diff --git a/examples/react_admin/backend/src/tasks/seed.rs b/examples/react_admin/backend/src/tasks/seed.rs new file mode 100644 index 0000000000..4e2544d90d --- /dev/null +++ b/examples/react_admin/backend/src/tasks/seed.rs @@ -0,0 +1,45 @@ +//! This task implements data seeding functionality for initializing new +//! development/demo environments. +//! +//! # Example +//! +//! Run the task with the following command: +//! ```sh +//! cargo run task +//! ``` +//! +//! To override existing data and reset the data structure, use the following +//! command with the `refresh:true` argument: +//! ```sh +//! cargo run task seed_data refresh:true +//! ``` + +use loco_rs::{db, prelude::*}; +use migration::Migrator; + +use crate::app::App; + +#[allow(clippy::module_name_repetitions)] +pub struct SeedData; +#[async_trait] +impl Task for SeedData { + fn task(&self) -> TaskInfo { + TaskInfo { + name: "seed_data".to_string(), + detail: "Task for seeding data".to_string(), + } + } + + async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { + let refresh = vars + .cli_arg("refresh") + .is_ok_and(|refresh| refresh == "true"); + + if refresh { + db::reset::(&app_context.db).await?; + } + let path = std::path::Path::new("src/fixtures"); + db::run_app_seed::(app_context, path).await?; + Ok(()) + } +} diff --git a/examples/react_admin/backend/src/views/auth.rs b/examples/react_admin/backend/src/views/auth.rs new file mode 100644 index 0000000000..2240a5087e --- /dev/null +++ b/examples/react_admin/backend/src/views/auth.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::users; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginResponse { + pub token: String, + pub pid: String, + pub name: String, + pub is_verified: bool, +} + +impl LoginResponse { + #[must_use] + pub fn new(user: &users::Model, token: &String) -> Self { + Self { + token: token.to_string(), + pid: user.pid.to_string(), + name: user.name.clone(), + is_verified: user.email_verified_at.is_some(), + } + } +} diff --git a/examples/react_admin/backend/src/views/mod.rs b/examples/react_admin/backend/src/views/mod.rs new file mode 100644 index 0000000000..f9bae3db2a --- /dev/null +++ b/examples/react_admin/backend/src/views/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod user; diff --git a/examples/react_admin/backend/src/views/user.rs b/examples/react_admin/backend/src/views/user.rs new file mode 100644 index 0000000000..9d830410fb --- /dev/null +++ b/examples/react_admin/backend/src/views/user.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::models::_entities::users; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CurrentResponse { + pub pid: String, + pub name: String, + pub email: String, +} + +impl CurrentResponse { + #[must_use] + pub fn new(user: &users::Model) -> Self { + Self { + pid: user.pid.to_string(), + name: user.name.clone(), + email: user.email.clone(), + } + } +} diff --git a/examples/react_admin/backend/src/workers/downloader.rs b/examples/react_admin/backend/src/workers/downloader.rs new file mode 100644 index 0000000000..8a608bfd72 --- /dev/null +++ b/examples/react_admin/backend/src/workers/downloader.rs @@ -0,0 +1,41 @@ +use std::time::Duration; + +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; +use tokio::time::sleep; + +use crate::models::users; + +pub struct DownloadWorker { + pub ctx: AppContext, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct DownloadWorkerArgs { + pub user_guid: String, +} + +#[async_trait] +impl BackgroundWorker for DownloadWorker { + fn build(ctx: &AppContext) -> Self { + Self { ctx: ctx.clone() } + } + + async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> { + // TODO: Some actual work goes here... + println!("================================================"); + println!("Sending payment report to user {}", args.user_guid); + + sleep(Duration::from_millis(2000)).await; + + let all = users::Entity::find() + .all(&self.ctx.db) + .await + .map_err(Box::from)?; + for user in &all { + println!("user: {}", user.id); + } + println!("================================================"); + Ok(()) + } +} diff --git a/examples/react_admin/backend/src/workers/mod.rs b/examples/react_admin/backend/src/workers/mod.rs new file mode 100644 index 0000000000..acb5733da2 --- /dev/null +++ b/examples/react_admin/backend/src/workers/mod.rs @@ -0,0 +1 @@ +pub mod downloader; diff --git a/examples/react_admin/frontend/.eslintrc.js b/examples/react_admin/frontend/.eslintrc.js new file mode 100644 index 0000000000..b66f2e247b --- /dev/null +++ b/examples/react_admin/frontend/.eslintrc.js @@ -0,0 +1,21 @@ +export default { + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/examples/react_admin/frontend/.gitignore b/examples/react_admin/frontend/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/react_admin/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/react_admin/frontend/README.md b/examples/react_admin/frontend/README.md new file mode 100644 index 0000000000..87d2d341b3 --- /dev/null +++ b/examples/react_admin/frontend/README.md @@ -0,0 +1,35 @@ +# react-admin + +## Installation + +Install the application dependencies by running: + +```sh +npm install +``` + +## Development + +Start the application in development mode by running: + +```sh +npm run dev +``` + +## Production + +Build the application in production mode by running: + +```sh +npm run build +``` + +## Authentication + +The included auth provider should only be used for development and test purposes. +You'll find a `users.json` file in the `src` directory that includes the users you can use. + +You can sign in to the application with the following usernames and password: +- janedoe / password +- johndoe / password + diff --git a/examples/react_admin/frontend/index.html b/examples/react_admin/frontend/index.html new file mode 100644 index 0000000000..f48d37138e --- /dev/null +++ b/examples/react_admin/frontend/index.html @@ -0,0 +1,125 @@ + + + + + + + + + react-admin + + + + + + + +
+
+
Loading...
+
+
+ + + diff --git a/examples/react_admin/frontend/package-lock.json b/examples/react_admin/frontend/package-lock.json new file mode 100644 index 0000000000..603697aa99 --- /dev/null +++ b/examples/react_admin/frontend/package-lock.json @@ -0,0 +1,9207 @@ +{ + "name": "react-admin", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "react-admin", + "dependencies": { + "axios": "^1.7.2", + "ra-data-json-server": "^5.0.5", + "react": "^18.3.0", + "react-admin": "^5.0.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/node": "^20.10.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "@vitejs/plugin-react": "^4.0.1", + "eslint": "^8.43.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^2.8.8", + "typescript": "^5.1.6", + "vite": "^4.3.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.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/core/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/@babel/generator": { + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "dependencies": { + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/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==" + }, + "node_modules/@emotion/babel-plugin/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/@emotion/cache": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.0.tgz", + "integrity": "sha512-hPV345J/tH0Cwk2wnU/3PBzORQ9HeX+kQSbwI+jslzpRCHE6fSGTohswksA/Ensr8znPzwfzKZCmAM9Lmlhp7g==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", + "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz", + "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz", + "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.9.0", + "@emotion/utils": "^1.4.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/styled": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", + "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz", + "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "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==", + "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.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "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==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz", + "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", + "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.4.tgz", + "integrity": "sha512-dBnh3/zRYgEVIS3OE4oTbujse3gifA0qLMmuUk13ywsDCbngJsdgwW5LuYeiT5pfA8PGPGSqM7mxNytYXgiMCw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.16.4", + "@mui/system": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/@mui/private-theming": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", + "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.16.4", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", + "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", + "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.16.4", + "@mui/styled-engine": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", + "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", + "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "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/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@remix-run/router": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.51.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.9.tgz", + "integrity": "sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.11.tgz", + "integrity": "sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A==", + "dependencies": { + "@tanstack/query-core": "5.51.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "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==", + "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/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==", + "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/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/autosuggest-highlight": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/autosuggest-highlight/-/autosuggest-highlight-3.3.4.tgz", + "integrity": "sha512-j6RETBD2xYnrVcoV1S5R4t3WxOlWZKyDQjkwnggDPSjF5L4jV98ZltBpvPvbkM1HtoSe5o+bNrTHyjPbieGeYA==", + "dependencies": { + "remove-accents": "^0.4.2" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "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/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.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "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.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "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==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "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==", + "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==" + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.833", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.833.tgz", + "integrity": "sha512-aVGP9xK70Ysrzip1m5LoJjCp1EDrYzZ7Pg/O3QR1h3PAhmc8SNfSXV3kmmtkg5rNO42EcTYmLX3eFMgqALlGIA==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "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==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@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-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/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/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/eslint-scope/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/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/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/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "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/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/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "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/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/file-selector/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "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.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "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==", + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hotscript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/hotscript/-/hotscript-1.0.13.tgz", + "integrity": "sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==" + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "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==", + "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==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.0.tgz", + "integrity": "sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==", + "engines": { + "node": ">=18.0.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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "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/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "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/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.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==" + }, + "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==", + "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-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==" + }, + "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/jsonexport": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonexport/-/jsonexport-3.2.0.tgz", + "integrity": "sha512-GbO9ugb0YTZatPd/hqCGR0FSwbr82H6OzG04yzdrG7XOe4QZ0jhQ+kOsB29zqkzoYJLmLxbbrFiuwbQu891XnQ==", + "bin": { + "jsonexport": "bin/jsonexport.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==" + }, + "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==" + }, + "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==", + "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/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.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "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==", + "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==", + "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/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/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/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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/node-polyglot": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-polyglot/-/node-polyglot-2.6.0.tgz", + "integrity": "sha512-ZZFkaYzIfGfBvSM6QhA9dM8EEaUJOVewzGSRcXWbJELXDj0lajAtKaENCYxvF5yE+TgHg6NQb0CmgYMsMdcNJQ==", + "dependencies": { + "hasown": "^2.0.2", + "object.entries": "^1.1.8", + "warning": "^4.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "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==", + "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/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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "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/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "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.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ra-core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-5.0.5.tgz", + "integrity": "sha512-3seifQurEnoaAWXxj9j/lVH2pXALcdY063l5CcRqE5AU79Dmt8MozOAeeUTeEFHgJ0SYGSLGbyWcqxY+TR8RZg==", + "dependencies": { + "@tanstack/react-query": "^5.8.4", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "eventemitter3": "^5.0.1", + "hotscript": "^1.0.12", + "inflection": "^3.0.0", + "jsonexport": "^3.2.0", + "lodash": "~4.17.5", + "query-string": "^7.1.3", + "react-error-boundary": "^4.0.13", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-hook-form": "^7.52.0", + "react-router": "^6.22.0", + "react-router-dom": "^6.22.0" + } + }, + "node_modules/ra-core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/ra-data-json-server": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-data-json-server/-/ra-data-json-server-5.0.5.tgz", + "integrity": "sha512-27P7/k05ksStA+CYuE/Jb3cw0G5OBqrf03kUYB2F44SvnkC4/tacLBJ3imGS7mWT/yDEoFxrXaCr/+O4qN4ZAw==", + "dependencies": { + "query-string": "^7.1.3", + "ra-core": "^5.0.5" + } + }, + "node_modules/ra-i18n-polyglot": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-5.0.5.tgz", + "integrity": "sha512-sJz+rn9X/XoJIz4TTXSL4xclfrZuRy0BxWmRmIej5o3AXGtQUdzitvV0eG2v27Yt71CvRGxZvar0uXr7jUZb5A==", + "dependencies": { + "node-polyglot": "^2.2.2", + "ra-core": "^5.0.5" + } + }, + "node_modules/ra-language-english": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-5.0.5.tgz", + "integrity": "sha512-uNu3uYieYn8+o020DCQfjQaxJ9HPkPYEZ3GW/nuv7SCb7hx+j6dZ/3OM+A8WfZo+T2IoJ+EOv0sL1MvixZXRbw==", + "dependencies": { + "ra-core": "^5.0.5" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-admin": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/react-admin/-/react-admin-5.0.5.tgz", + "integrity": "sha512-jSijZcGHGiX3LYyatKF7F9bWRgI7sZKdFNZRVY/0HWWQuxCdvj4W/xFCzDR1tGcwaHHXnnCQtNt1raucAyFkhQ==", + "dependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/icons-material": "^5.15.20", + "@mui/material": "^5.15.20", + "ra-core": "^5.0.5", + "ra-i18n-polyglot": "^5.0.5", + "ra-language-english": "^5.0.5", + "ra-ui-materialui": "^5.0.5", + "react-hook-form": "^7.52.0", + "react-router": "^6.22.0", + "react-router-dom": "^6.22.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-admin/node_modules/ra-ui-materialui": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-5.0.5.tgz", + "integrity": "sha512-ahBAYpakn5OVpPx57vFQYfIdtHjESzO5yYpZ+R+gwpNDHE0Eg0+VMw2G8/xKRymnU/smMk0jqhmR/RghUn80Gg==", + "dependencies": { + "@tanstack/react-query": "^5.8.4", + "autosuggest-highlight": "^3.1.1", + "clsx": "^2.1.1", + "css-mediaquery": "^0.1.2", + "dompurify": "^2.4.3", + "hotscript": "^1.0.12", + "inflection": "^3.0.0", + "jsonexport": "^3.2.0", + "lodash": "~4.17.5", + "query-string": "^7.1.3", + "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.0.13", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "@mui/icons-material": "^5.15.20", + "@mui/material": "^5.15.20", + "ra-core": "^5.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-hook-form": "*", + "react-is": "^18.0.0", + "react-router": "^6.22.0", + "react-router-dom": "^6.22.0" + } + }, + "node_modules/react-admin/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "peer": true + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "dependencies": { + "@remix-run/router": "1.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "dependencies": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remove-accents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.4.tgz", + "integrity": "sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg==" + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "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-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==", + "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==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "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==", + "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==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "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/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "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.2", + "picocolors": "^1.0.1" + }, + "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/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "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/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "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": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true + }, + "@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "requires": { + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" + }, + "@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true + }, + "@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dev": true, + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + } + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==" + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/runtime": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@emotion/babel-plugin": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + }, + "dependencies": { + "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==" + }, + "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==" + } + } + }, + "@emotion/cache": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.0.tgz", + "integrity": "sha512-hPV345J/tH0Cwk2wnU/3PBzORQ9HeX+kQSbwI+jslzpRCHE6fSGTohswksA/Ensr8znPzwfzKZCmAM9Lmlhp7g==", + "requires": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "@emotion/is-prop-valid": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", + "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", + "requires": { + "@emotion/memoize": "^0.9.0" + } + }, + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "@emotion/react": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz", + "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz", + "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==", + "requires": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.9.0", + "@emotion/utils": "^1.4.0", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "@emotion/styled": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", + "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0" + } + }, + "@emotion/unitless": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz", + "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "requires": {} + }, + "@emotion/utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" + }, + "@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": 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.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "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.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.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": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mui/core-downloads-tracker": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz", + "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==" + }, + "@mui/icons-material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", + "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "requires": { + "@babel/runtime": "^7.23.9" + } + }, + "@mui/material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.4.tgz", + "integrity": "sha512-dBnh3/zRYgEVIS3OE4oTbujse3gifA0qLMmuUk13ywsDCbngJsdgwW5LuYeiT5pfA8PGPGSqM7mxNytYXgiMCw==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.16.4", + "@mui/system": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + } + } + }, + "@mui/private-theming": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", + "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.16.4", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", + "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", + "requires": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", + "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.16.4", + "@mui/styled-engine": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", + "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", + "requires": {} + }, + "@mui/utils": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", + "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", + "requires": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + } + } + }, + "@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" + } + }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, + "@remix-run/router": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==" + }, + "@tanstack/query-core": { + "version": "5.51.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.9.tgz", + "integrity": "sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==" + }, + "@tanstack/react-query": { + "version": "5.51.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.11.tgz", + "integrity": "sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A==", + "requires": { + "@tanstack/query-core": "5.51.9" + } + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "requires": { + "@types/react": "*" + } + }, + "@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@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==", + "dev": true + }, + "@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "requires": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + } + }, + "acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true + }, + "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" + } + }, + "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==", + "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 + }, + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } + }, + "array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, + "autosuggest-highlight": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/autosuggest-highlight/-/autosuggest-highlight-3.3.4.tgz", + "integrity": "sha512-j6RETBD2xYnrVcoV1S5R4t3WxOlWZKyDQjkwnggDPSjF5L4jV98ZltBpvPvbkM1HtoSe5o+bNrTHyjPbieGeYA==", + "requires": { + "remove-accents": "^0.4.2" + } + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "dependencies": { + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + } + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "caniuse-lite": { + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "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==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "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==", + "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==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "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" + } + }, + "css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" + }, + "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 + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.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" + } + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dompurify": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==" + }, + "electron-to-chromium": { + "version": "1.4.833", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.833.tgz", + "integrity": "sha512-aVGP9xK70Ysrzip1m5LoJjCp1EDrYzZ7Pg/O3QR1h3PAhmc8SNfSXV3kmmtkg5rNO42EcTYmLX3eFMgqALlGIA==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + } + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + } + }, + "es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "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==" + }, + "eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@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" + }, + "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 + }, + "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" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "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-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "requires": {} + }, + "eslint-plugin-react": { + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "requires": {} + }, + "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" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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 + }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "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-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, + "requires": { + "@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" + }, + "dependencies": { + "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, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "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 + }, + "fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "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" + } + }, + "file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "requires": { + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + } + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==" + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "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==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "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 + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "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 + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + } + }, + "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" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "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==" + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "hotscript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/hotscript/-/hotscript-1.0.13.tgz", + "integrity": "sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==" + }, + "ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "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==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.0.tgz", + "integrity": "sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==" + }, + "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 + }, + "internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "requires": { + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "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-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true + }, + "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-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "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-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.14" + } + }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "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==" + }, + "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-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==" + }, + "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 + }, + "jsonexport": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonexport/-/jsonexport-3.2.0.tgz", + "integrity": "sha512-GbO9ugb0YTZatPd/hqCGR0FSwbr82H6OzG04yzdrG7XOe4QZ0jhQ+kOsB29zqkzoYJLmLxbbrFiuwbQu891XnQ==" + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "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" + } + }, + "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==" + }, + "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==" + }, + "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==", + "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" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "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==" + }, + "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==", + "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" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "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 + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node-polyglot": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-polyglot/-/node-polyglot-2.6.0.tgz", + "integrity": "sha512-ZZFkaYzIfGfBvSM6QhA9dM8EEaUJOVewzGSRcXWbJELXDj0lajAtKaENCYxvF5yE+TgHg6NQb0CmgYMsMdcNJQ==", + "requires": { + "hasown": "^2.0.2", + "object.entries": "^1.1.8", + "warning": "^4.0.3" + } + }, + "node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "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==" + }, + "object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + } + }, + "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" + } + }, + "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==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "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==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true + }, + "postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + } + }, + "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 + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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==" + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "requires": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "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 + }, + "ra-core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-5.0.5.tgz", + "integrity": "sha512-3seifQurEnoaAWXxj9j/lVH2pXALcdY063l5CcRqE5AU79Dmt8MozOAeeUTeEFHgJ0SYGSLGbyWcqxY+TR8RZg==", + "requires": { + "@tanstack/react-query": "^5.8.4", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "eventemitter3": "^5.0.1", + "hotscript": "^1.0.12", + "inflection": "^3.0.0", + "jsonexport": "^3.2.0", + "lodash": "~4.17.5", + "query-string": "^7.1.3", + "react-error-boundary": "^4.0.13", + "react-is": "^18.2.0" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + } + } + }, + "ra-data-json-server": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-data-json-server/-/ra-data-json-server-5.0.5.tgz", + "integrity": "sha512-27P7/k05ksStA+CYuE/Jb3cw0G5OBqrf03kUYB2F44SvnkC4/tacLBJ3imGS7mWT/yDEoFxrXaCr/+O4qN4ZAw==", + "requires": { + "query-string": "^7.1.3", + "ra-core": "^5.0.5" + } + }, + "ra-i18n-polyglot": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-5.0.5.tgz", + "integrity": "sha512-sJz+rn9X/XoJIz4TTXSL4xclfrZuRy0BxWmRmIej5o3AXGtQUdzitvV0eG2v27Yt71CvRGxZvar0uXr7jUZb5A==", + "requires": { + "node-polyglot": "^2.2.2", + "ra-core": "^5.0.5" + } + }, + "ra-language-english": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-5.0.5.tgz", + "integrity": "sha512-uNu3uYieYn8+o020DCQfjQaxJ9HPkPYEZ3GW/nuv7SCb7hx+j6dZ/3OM+A8WfZo+T2IoJ+EOv0sL1MvixZXRbw==", + "requires": { + "ra-core": "^5.0.5" + } + }, + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-admin": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/react-admin/-/react-admin-5.0.5.tgz", + "integrity": "sha512-jSijZcGHGiX3LYyatKF7F9bWRgI7sZKdFNZRVY/0HWWQuxCdvj4W/xFCzDR1tGcwaHHXnnCQtNt1raucAyFkhQ==", + "requires": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/icons-material": "^5.15.20", + "@mui/material": "^5.15.20", + "ra-core": "^5.0.5", + "ra-i18n-polyglot": "^5.0.5", + "ra-language-english": "^5.0.5", + "ra-ui-materialui": "^5.0.5", + "react-hook-form": "^7.52.0", + "react-router": "^6.22.0", + "react-router-dom": "^6.22.0" + }, + "dependencies": { + "ra-ui-materialui": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-5.0.5.tgz", + "integrity": "sha512-ahBAYpakn5OVpPx57vFQYfIdtHjESzO5yYpZ+R+gwpNDHE0Eg0+VMw2G8/xKRymnU/smMk0jqhmR/RghUn80Gg==", + "requires": { + "@tanstack/react-query": "^5.8.4", + "autosuggest-highlight": "^3.1.1", + "clsx": "^2.1.1", + "css-mediaquery": "^0.1.2", + "dompurify": "^2.4.3", + "hotscript": "^1.0.12", + "inflection": "^3.0.0", + "jsonexport": "^3.2.0", + "lodash": "~4.17.5", + "query-string": "^7.1.3", + "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.0.13", + "react-transition-group": "^4.4.5" + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "peer": true + } + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "requires": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + } + }, + "react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, + "react-hook-form": { + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", + "requires": {} + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true + }, + "react-router": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "requires": { + "@remix-run/router": "1.18.0" + } + }, + "react-router-dom": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "requires": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + } + }, + "remove-accents": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.4.tgz", + "integrity": "sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg==" + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "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==" + }, + "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" + } + }, + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "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-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + } + }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.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 + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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==" + }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true + }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==" + }, + "string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "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 + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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==" + }, + "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==" + }, + "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" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "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 + }, + "typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } + }, + "typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + } + }, + "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" + } + }, + "vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "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" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "requires": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "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 + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "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/examples/react_admin/frontend/package.json b/examples/react_admin/frontend/package.json new file mode 100644 index 0000000000..ba5f103ed9 --- /dev/null +++ b/examples/react_admin/frontend/package.json @@ -0,0 +1,34 @@ +{ + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "type-check": "tsc --noEmit", + "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src", + "format": "prettier --write ./src" + }, + "dependencies": { + "axios": "^1.7.2", + "ra-data-json-server": "^5.0.5", + "react": "^18.3.0", + "react-admin": "^5.0.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/node": "^20.10.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "@vitejs/plugin-react": "^4.0.1", + "eslint": "^8.43.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^2.8.8", + "typescript": "^5.1.6", + "vite": "^4.3.9" + }, + "name": "react-admin" +} diff --git a/examples/react_admin/frontend/prettier.config.js b/examples/react_admin/frontend/prettier.config.js new file mode 100644 index 0000000000..7c6d6c73d3 --- /dev/null +++ b/examples/react_admin/frontend/prettier.config.js @@ -0,0 +1 @@ +module.exports = {} \ No newline at end of file diff --git a/examples/react_admin/frontend/public/favicon.ico b/examples/react_admin/frontend/public/favicon.ico new file mode 100644 index 0000000000..f9222554e6 Binary files /dev/null and b/examples/react_admin/frontend/public/favicon.ico differ diff --git a/examples/react_admin/frontend/public/manifest.json b/examples/react_admin/frontend/public/manifest.json new file mode 100644 index 0000000000..0a023e7fa7 --- /dev/null +++ b/examples/react_admin/frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "react-admin", + "name": "{{name}}", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react_admin/frontend/src/App.tsx b/examples/react_admin/frontend/src/App.tsx new file mode 100644 index 0000000000..5fdcf27215 --- /dev/null +++ b/examples/react_admin/frontend/src/App.tsx @@ -0,0 +1,15 @@ +import { Admin } from 'react-admin'; +import { Layout } from './Layout'; +import { authProvider } from './authProvider'; + + +export const App = () => ( + + + +); + + \ No newline at end of file diff --git a/examples/react_admin/frontend/src/Layout.tsx b/examples/react_admin/frontend/src/Layout.tsx new file mode 100644 index 0000000000..ab303b7e8c --- /dev/null +++ b/examples/react_admin/frontend/src/Layout.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from 'react'; +import { + Layout as RALayout, + CheckForApplicationUpdate, +} from "react-admin"; + +export const Layout = ({ children }: { children: ReactNode }) => ( + + {children} + + +); diff --git a/examples/react_admin/frontend/src/authProvider.ts b/examples/react_admin/frontend/src/authProvider.ts new file mode 100644 index 0000000000..40db29b374 --- /dev/null +++ b/examples/react_admin/frontend/src/authProvider.ts @@ -0,0 +1,44 @@ +import { AuthProvider, HttpError } from 'react-admin'; +import data from './users.json'; + +/** + * This authProvider is only for test purposes. Don't use it in production. + */ +export const authProvider: AuthProvider = { + login: ({ username, password }) => { + const user = data.users.find( + u => u.username === username && u.password === password + ); + + if (user) { + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + let { password, ...userToPersist } = user; + localStorage.setItem('user', JSON.stringify(userToPersist)); + return Promise.resolve(); + } + + return Promise.reject( + new HttpError('Unauthorized', 401, { + message: 'Invalid username or password', + }) + ); + }, + logout: () => { + localStorage.removeItem('user'); + return Promise.resolve(); + }, + checkError: () => Promise.resolve(), + checkAuth: () => + localStorage.getItem('user') ? Promise.resolve() : Promise.reject(), + getPermissions: () => { + return Promise.resolve(undefined); + }, + getIdentity: () => { + const persistedUser = localStorage.getItem('user'); + const user = persistedUser ? JSON.parse(persistedUser) : null; + + return Promise.resolve(user); + }, +}; + +export default authProvider; diff --git a/examples/react_admin/frontend/src/dataProvider.ts b/examples/react_admin/frontend/src/dataProvider.ts new file mode 100644 index 0000000000..2d2c390d4e --- /dev/null +++ b/examples/react_admin/frontend/src/dataProvider.ts @@ -0,0 +1,90 @@ +import { DataProvider } from "react-admin"; +import axios from 'axios'; + +const apiUrl = 'http://localhost:3000/api/graphql'; + +export const dataProvider: DataProvider = { + // Fetch data for post listing + getList: (resource, params) => { + // Paginator status + const { page, perPage } = params.pagination; + // Sorter status + const { field, order } = params.sort; + + // POST request to GraphQL endpoint + return axios.post(apiUrl, { + query: ` + query { + notes ( + orderBy: { ${field}: ${order} }, + pagination: { page: { limit: ${perPage}, page: ${page - 1} }} + ) { + nodes { + id + title + createdAt + updatedAt + } + paginationInfo { + pages + current + offset + total + } + } + } + ` + }) + .then((response) => { + // Unwrap the response + const { nodes, paginationInfo } = response.data.data.notes; + // Return the data array and total number of pages + return { + data: nodes, + total: paginationInfo.total, + }; + }); + }, + + // Fetch data for a single post + getOne: (resource, params) => { + // POST request to GraphQL endpoint + return axios.post(apiUrl, { + query: ` + query { + notes(filters: {id: {eq: ${params.id}}}) { + nodes { + id + title + content + createdAt + updatedAt + } + } + } + ` + }) + .then((response) => { + // Unwrap the response + const { nodes } = response.data.data.notes; + // Return the one and only data + return { + data: nodes[0], + }; + }); + }, + + getMany: (resource, params) => { }, + + getManyReference: (resource, params) => { }, + + update: (resource, params) => { }, + + updateMany: (resource, params) => { }, + + create: (resource, params) => { }, + + delete: (resource, params) => { }, + + deleteMany: (resource, params) => { }, +}; diff --git a/examples/react_admin/frontend/src/index.tsx b/examples/react_admin/frontend/src/index.tsx new file mode 100644 index 0000000000..2ff84b7a9b --- /dev/null +++ b/examples/react_admin/frontend/src/index.tsx @@ -0,0 +1,33 @@ +import ReactDOM from 'react-dom/client'; +import { Admin, Resource, List, Datagrid, TextField, Show, SimpleShowLayout } from 'react-admin'; +import { dataProvider } from './dataProvider'; + +const PostList = () => ( + + + + + + + + + +); + +const PostShow = () => ( + + + + + + + + + +); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/examples/react_admin/frontend/src/users.json b/examples/react_admin/frontend/src/users.json new file mode 100644 index 0000000000..779908e298 --- /dev/null +++ b/examples/react_admin/frontend/src/users.json @@ -0,0 +1,18 @@ +{ + "users": [ + { + "id": 1, + "username": "janedoe", + "password": "password", + "fullName": "Jane Doe", + "avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gKgSUNDX1BST0ZJTEUAAQEAAAKQbGNtcwQwAABtbnRyUkdCIFhZWiAH3wAIABMAEgAWADFhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAAADhjcHJ0AAABQAAAAE53dHB0AAABkAAAABRjaGFkAAABpAAAACxyWFlaAAAB0AAAABRiWFlaAAAB5AAAABRnWFlaAAAB+AAAABRyVFJDAAACDAAAACBnVFJDAAACLAAAACBiVFJDAAACTAAAACBjaHJtAAACbAAAACRtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABwAAAAcAHMAUgBHAEIAIABiAHUAaQBsAHQALQBpAG4AAG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAMgAAABwATgBvACAAYwBvAHAAeQByAGkAZwBoAHQALAAgAHUAcwBlACAAZgByAGUAZQBsAHkAAAAAWFlaIAAAAAAAAPbWAAEAAAAA0y1zZjMyAAAAAAABDEoAAAXj///zKgAAB5sAAP2H///7ov///aMAAAPYAADAlFhZWiAAAAAAAABvlAAAOO4AAAOQWFlaIAAAAAAAACSdAAAPgwAAtr5YWVogAAAAAAAAYqUAALeQAAAY3nBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACltwYXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAAKW2Nocm0AAAAAAAMAAAAAo9cAAFR7AABMzQAAmZoAACZmAAAPXP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAgAMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAABAAUGBwIDBAj/xAA2EAABAwMBBgMHAwQDAQAAAAABAAIDBAUREgYTITFBUSJxgRQyYZGxwdEHQqEkUmLhFRYjkv/EABoBAAIDAQEAAAAAAAAAAAAAAAIDAAEEBQb/xAAkEQACAgIDAAEEAwAAAAAAAAAAAQIRAyEEEjETBSJRcTJBYf/aAAwDAQACEQMRAD8AuIIoIogAooIqygorB72xsc97g1rRkk9FFrlfnVD3Rwu0Qj5uQyko+hRi5eEjlr6aEkOlBI6N4rSbpCG6j4R0zzPoFEIpZpn+Ahv+TuJ9E9Qtipodc0haD15ud5BI+Vsb8aXoq/a6GgJ1UsrgBknTj6rgp/1JtEkgjnbNTk9Xxkt/+m5Cbrvbam9sc2XeU9G0jTFry6Q9M/hddk/T6kpId7UwNmqJCOBOGxjoD3KLvIroiX0VxprhCJaeVr2njlpyutR+k2dlttQZ4KwMZpwIGjwD5lbI785leaSppZWHmJG+JpHfKYppgOLQ9pIAhwBHIpIwBJFJBQhgiEAiFRYQkgmzaCuNDaZHsOJHeBvmVG6VkSt0Me0l93spoqY5Y0+N3Qn8JgjdrcA3xOPU9f8AS485Jy4c8ucep/CzirNBIh4D9zz1WGU7dmyMElRIIXspeBw+fGeJ4N8/wtsEstTUtHFzz1Pb7BNVHvJQA0EAnOo83fH/AGpBR7qniw14yfekz9+qFMNxHiljYwjhqc3kTxPxwnaIZA+6ZoJWDGhjnZ68gnaEvewftHw4BOixTVGc2GsOdI4c3KFXmSuq75Rw0kQ3EWZKieQaQG8g0D4n7KauaGgiNu8eeJJPBN89JLUN8btWg5HDAL++OwUZSMbTWxV9vZNFqxyIcMEELuTdTBtNUbiP3A0D5BOC0QdozzVMWUksoIwTBFBIKEMlFdtJSIKaMHHEuKlKg+3Ly6rgizhugl3wHVLyv7WMxK5Ih8k2+IAJEecNGeLvj5LvoossDsZHl9E2x6ZZNTvDGPp2WFde45P6WlqI4tPAZIGT6rAb0vwSiOox4RgA+84nn6ruiuAhLdA1n+5x5Kt6SsrYagtqqkyuJ8Jxj+FLpIql9qM0b9GW8Ceioaoa2TigrXStDsMb8SOKeYtUgGpxLe5+wVQ2eouftIabs52T7ugHPqrOtNNX7gb+r3zSPE1zNJPljknQaehGSFDo6piax5Em7iZ78h+gXJV1OId7lzIgzIYOePyue4QvxGGs8OsAN6D/ACPfH1QinirgZmODoi4Mb5BFYlqjfSsc58kjxgkN4dua61wWWqFbb2VGCDM4uAPMN6LsJ4p+LwRlM8pZWGUU0UBFYhFUWFV/t0S67xR8muhGT8MqwFDNvKJz4IqxgyWjQ49uyXl3EbidSKxrp31VWaKnJaxoySO6aX7INZG9swlcHuD3OyMkjlxKdLWA2uqJH/3NH8KRVNWz2XJ7LCm1tHSUIyWyL0NvdC6CnBeQHjRqOSB2VwVdnd/1YwRt/wDQx8Pkq4sX9XdoXubpZrGCeo7q7i3eW0OjbqLW8B3RRjdkm+tJHn6usdbV1JY6rnpmBww5jT4QPLn6q09jLdcKBsRgv0lZSFgaaaoYXBuBza4kuB7g5HwCa6y50tRWvi3ZjeHYc1wwQVMdm42NaC3lhXBu6JkikuzHS4QuNuqCPf3biPPCqj9OdonubHa6txLyXkPJ65JH1x6K27tKYrXVPAyRE/A7nBVE7OUEtJtDSs0k6JwHO8zghMemZ6bjZcdqjZT0jo2DAY4tC68rVBHuosY4klx81sWnGqiYcjuRkllAIhGCBFBFUWFc9dSR11FLTyDLXtI8l0BJUXZRt4tNRZ7hPFI3Trw5p6HHBcEk0jgwO93l6q3tsrJ/y1oL4WZqYTqZ8R1H0VRVEDKqikgkaQRkdiCsOWHWR0+Pk7RFRzVNHXQmNwc1pHAHBVq2m+19QIzTgMhaMFsjclx+ap2wW6gn3dNXvqIpA7G+DstcB9CrVtVqsVDa4Zpa2eUmPIDS4knIzgD4FUou9Dvtqpe/oZ9qrfUR1Elw3ZDy7UeGAVKtibgKi3skzwPDj0KiV7ornc6xk8EtdTW+QhraSZ+S49SW8cAeamWzlsFupBG39ztXkotSKl/CmO21N3pbLs7U3GtL/Z4tOsMGScuAwB6qK7JUrbs1t6ex+5kOuESBoceJ4uDeGfJP+1NpO0NLSWyRgNE6cS1Rzza3iGjzOPknKCnipadkEEbY4o2hrWtGAAFojj7O2YJ5eq6oyKCywlhaTIBEJJKEMQigEVRYUViioQJAIIPVVrtzYW0FY2507cQVLtMoH7X9/X6hWVlcl0pKWvtlRTVuPZ3sOs5xpxx1A9COaDJDtGhuKbhKyjW2uT2newSuZq544g+in2ytGYpGzTPMj28WgNAwofRV0cFU6F7w5oOGudw1DoVPbRdKCBrdU0eojg1pyT6BYbr+zsd5dKRIvZN6/ey4yPhyXRTx+PDeXfssaZz6wBzssj6N6nzTiyJrAABgI1vaMrdaZp06SQTniktkvv5WtbIO4o5+RVJgwkikjAAkigVCGsJZWuSaOGMySvaxg5uccAJiqtqqeNxbSxOmP9x8I/KCU4x9DjCUvESIJuvG0Fp2fp2zXWvhpWPzo3h4uxzwBxKi1XtTcJGnS5sDe7Bx+ZVD7WX+p2gvs1TNM+VkZ3cOp2fCD9+amOayPReTG4LZaF2/XVjKmRlotTZIG5DZal5Bce+kch5lcLNqL5erSJrhXPd7SNRiYA2No6AAKocHGOqs21NJs1K3HERgfwg5T6xSQzixTk2zAtEziCn6wwbmoa4DHHoE1tgO9BA9FJLfTua0ODeK5sjpw0WFaagua0Ek+akAcC3KiNnbI0AkKTsfiLJTsb0IyrZue3U3yULuW39stV+fbKlkmmMAPmZ4sPPTCeNoL9HZbPUVbiCWN8I7novPE9TLVVktTM4ukkLnuJ6kldPg4fkk2/DncyfSKr09C2zaizXZzI6SvidM/lE7wv8AkU7rznsdUvG1Nvbk5FWz6r0O2XuPkj5EYYpJJ+isPfIm6NqCQcDySSk0/A2mvStrpepLtWu0kimYcRt+5+K1sZlqa6LmMp13ga1cuUnJ2zrQioqkM21VULfs7VzA4foLWeZ4fdUnjAHmrI/Uiv8A6OmpQffeXkfAD8lVyBlkY75K6XDhWO/yc7ly++vwGMZljGM5I+qvWlpab2eMNiDQGjgAqNb4J2EftwR6cV6Et0Daihp5WjwyRtcPUIebGkhnCabZjTWykmOHsHDkU7U9BHF7ucLXFTljuSc4W8srnUb26OmmeI2Dgt7qzIw44C1BgwmLaq7w2a0S1Dj4sYYM8S7oEyKb0hcmvWRD9S9pI6qaCz0rsiM7ydw79B6c/kq/Mni7cM/RYmd9XUyVEpJe4l7s9StD3nWT105+y9NxsXw4lE8/nyfLkch32Lk07X21zjw3+r5L0HHUa25yvPWxzc7YW9nYk/wVdrKgsAHRcn6lKpx/R0+BG4N/6P8AFL4hxTg2IvZkc1HqSoy8EqUUUgfGFjxt3o0ZUq2f/9k=" + }, + { + "id": 2, + "username": "johndoe", + "password": "password", + "fullName": "John Doe", + "avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wCEAAIDAwMEAwQFBQQGBgYGBggIBwcICA0JCgkKCQ0TDA4MDA4MExEUEQ8RFBEeGBUVGB4jHRwdIyolJSo1MjVFRVwBAgMDAwQDBAUFBAYGBgYGCAgHBwgIDQkKCQoJDRMMDgwMDgwTERQRDxEUER4YFRUYHiMdHB0jKiUlKjUyNUVFXP/CABEIAMgAyAMBIgACEQEDEQH/xAAeAAAABwEBAQEAAAAAAAAAAAACAwQFBgcICQEACv/aAAgBAQAAAADX6pYoNMMEMQvRe+eB+D4EASSyFak00wYxC+b86V6li1h6xe/ghAUWWpUGmDGKO5qp3BTO6zOKQy5O1ct+8AWAB5xpgxQDkw+xsqAXM4QKo2OX9s7MF8Arw84Znv3MzFNrkP8AnHRLBUzSgY9dbbUageywnHGC+h3COtrbrVDNrLiyWIIUBPTCz73tiorePGZ77z55KntLoGw5+4qq4Y08L6XaFu6N4a2od99H3Xmnk5Jc2wWnSVjZnzTDIi61ruyXT6vaL3TmjT1fZc2dlmhdrdM5S/rfsY4BjVnveRbXediVlgXvIuWYDDoq1cR6QtGfu7kKvK9Xkn8pM/P+pFeVun+FdU0tDOnFh5FsWTXTIz/CTE7QXkTnpJrpIqrrtHkda0pVWh6Csywc3pJLrHQUwRs1Ut+WRzWPQLROT4toepGy94vzV6PT/n3ojfGkBs5aCIZh9d1rVL46iuoml5WsyXrDZUah83WoG5gm2aa7PbgjkCdwWnEqRZq6ETiJrVLYpj8DQSGdoGQ5XQ4I4rkTf8pYbnhdokWEfD6jbrwoaUv57o+Q2JVlL7Nhb3Im7jTrqz/tgHZkrC3tJxFc5LlBTT8rWrPTlXuG+du4rImJFT5P6xSB8k3oiyUKk44Y/ffE/EBd1KaMp4t0d0vT1rO594WQ+fBF8X8UTzD5GdB9sZ/yHc/VactaPiTYG/tL+J/Bm/ehhPFTA31i35a2ibu01YTS5R782Ee171cspT8MY+fXKKrGn65+hdo7alYJOTKpxxw5QEG/pSNOHRPI6o/E9bsct/TZbbarZ1rvOl8W/M9TRH6TVKblDgqRoXVC0JD/ANV9mEtrU1zByLKwtwCS/o/a+H9HKZfFXAI2j3uF0rMH7594EJPBDCH/xAAbAQACAwEBAQAAAAAAAAAAAAAEBQECAwYAB//aAAgBAhAAAAD7PjSlfZYkXm1hs6V51HkM46smbD51V81FIMOLcbc9lGQq1fVuWz9vyCpc/wCJaHrjDHIXVcGvGe8u2wIupayPqSrG6kLDYcVHnM4xt1URULr0MHyJqQQkW3JYstIJ9PrruQch27bGtZk1t7hubMN7M4FZu43rKXnxnJnQ5890cTFFGXvea4//xAAbAQABBQEBAAAAAAAAAAAAAAAEAAIDBQYBB//aAAgBAxAAAADwyeR7+yzjxJrSJXP1OgmKo8YMmESPuNUnyBV+cGi0syIMt7PlIHmF3WFcM9Goay2BBxNpkvRHgzeq5x1fFpfMSxbaBti60lqj+1j+Hu5G4tMPzA7BXkRQN9OEHErM8xQcSj1Ooop+YORySCqH+m31WFjawywhqY+N0t0XSBU8N7nHu6ribqQMf//EACUQAAAHAAIDAAIDAQAAAAAAAAECAwQFBgcSEwAIERAUFRYXGP/aAAgBAQABAgAqZUipAl19fX19fDhw6+vhwEgkEnWKYpimKYplSKkCfWBATBPrAnDg/eON8/6WH2ZZe0Fb9g42T4cOsSdYpimVMqYJgQCAThwm56U3+7bdJPwjYuCPW35TNM70au2ICcBIKYk4FIUgEAgE4cLnaLRPQLGThywrpSMQm2DpB1JnQzq/0W5D4HgkEgkAgEAnACceO9Wdu8ippq5lp9KRoaMxYE1khcuBJgV0mNMhNEjXwkEgEAgEAgF42uct08zCQtLuYWkoNjKRr1mBFEAaLCh5HoNazWGKyktcgEB5Afnz9n7W9EHXaqxMFeeup1nT5+SWRFqo2VLljuCtEe/ttkeSH9sUvCF0Pcj6KF13qbWL+rXs0Jh6PrNWvU+QwGzZZKwzt8qkdg+SZ1yGPCy9ydO3kwwuaWfUq5xhiRbXWaDVqfQ8Xj2iaSQpmWHUHL+vjnjPJnmcS0K5spZXIJHZ137ljXArL9adm1qzVqvZatHwzN0z8QMVNMpxmK4hmS9PY150w1ZNo6dusSca2c8bfJdlqNbC4QrCSrHiUPORhq7DuI9UqpVVQKPxYnF3EWqnvaUlVMwi9QTNFFjgqxHdwAxUr1XNI1PVajrku+iNOX3KEs8AjFvgBfxDy02K/Z5aISOgq9G3iAXgZuQslvqBnUbK11jXmMDZ6M9jMEJaM3n84qWGVXKkWCgrHaGn4GXcIOgeoKuTD4qLmIaQsYnMokYNwMlq6vr1CHM/jC1lqkZdRVY8Yo2Rvi6NZVhhXM7L4dXmmAKiINgbGHU42hQyvkjIoOxVMsU8i4iZKLvEIANHEYSvDXU6/wDx6rQ0g3cFaKeF8FCVjaVfpTcmhmzVPwyEg+l5ehrmZG8QlkpQH6TpN4urILfxUTGuEVWsU1kWEXHad5TqyzX/ANGr8vJO7FYJm0Yyi8FaCSgCQpIn9EWoRYQ5IgsaLYGJWQNgab7mlRpsFk3+AQ538zOS5FavHPDRQdHUJBIJAaFa/r9AI9YF+fVyXRagScJD2WNs87ZrRjlJYkceaFbKVfhX7zLir3i5/aByLj9oXAuDL7l7SwEtWtQgNmsu4Sc/mWYx7VApiyTLSM7p/tLTd7+iIOBWBUFe0VBUtNy1/wBnfoDDSEZXY2kQ0LCtYhqUqAdOhZlLRSw5f7BU/UwW7e0FOZ19Q9obPMP2HgeZYtI0it1muVVSGQbFFsm2bgX2wyUDgimcEyh4BtD2W/bRDNHyqhHMcZKruqjQ29KO1BJ0ikZig1Q8nIbV6IUwj2kPJSuo+yxUY1FBRZQBcAJEwz+eAVURj3UYhCtmf59lsxfMxADzE7quvoopHQJ9bEMRM6iHz02uwG+8vvn38qF9jssg47//xAA/EAACAQMCBAQDBQYFAgcAAAABAgMABBEFIQYSMUETIlFhFDJxBxBCUoEjMFNigpEgJKGxwRWyNEBDcqLD0f/aAAgBAQADPwD2ofvhQH/kR99lYWzz3dxHBEgJZ5WCKB9TX2XRSSoNXaQpneOCRlYj0OK4TGrTwGzuGtV5BHdJuGJ65TY1wwJLrGl3johIiIwGfA2znZc14s/LJww/JnqlxuE+jCuGNS1KeO9AsY+YiIyNk8vYEAHLVp+o24mtZhJGejYI/en79D0SzN1qWoQWkI/HK/Ln2A6muF0mSLTLO6v+Z+UShfDT+nm3ate+FeGyUWTMDl1YSSgfy/hX61rOqSNLdXE83OdvFkaRnP8AVTjBZs5GcCrm/eXkISKADxZOy57D1Y1Etkk7xsFkGYE6ZUHHNjqebsa5IxDAMLv4kijdyNsKfSpBOsKoTJJ8qgZ9q4g4L1yBjPK9lz8tzaFwysvflG4DCtF1/To7zTryO4hcAhkYEjPZh1DDuD9wofu7ThrQLm/mAYphIkP45H2Va1TXLmDUb29iuLlsmGPAZIFyfPJ2B/KtSWuny3CIwjkID3cqhpJC3RY1PT2AFeFA17qUiwRsQILXPNLKTuCwG5J9KaOCWe6TwGVA7oVyY0b5QR/FbsOwp3kWxtYB8VcEc/rGn5N+nqx6k1ZWtpY6Rb+HK6I094cll5mbdpCOwAAx+LoKnMlpbIzLNcN5S48ybZLEey9B2yK0ZDKsPhg28jq5LcwRIV83132HqavLe8uJbacWqTKijHnkbC4IX/8Aaj+CxDpzlc5aSRcPk9ixI/sBVzwXxCt1Ekj28mEu7dScSJ6r6OvVTWjcV6Il9YT86hikgI5XVuoDr2JFKOpFKeh/dGvj9fj00TFbKwTmmZepkfY/r2FKW55SIoQfKvQCtT1NxLGn+XtkJ52B5UQ7Zb2PYdWqBb06nqErSP5vBMm5X3x606aSGhQm4dyyhhkQAjJmmJ2Mp7D8Io28cgh3eZiDKd2Pq2TUWm2z39wQ6Kwc87BA8g+UEn8AqwvNQur/AOIubl90VIE5UGW5zzStgZZvSlmR40VYA3znmLN6+c9BUETElM425+bdqjZWwhiUn5FJwf1bJJoDDFjv03oaFxRNaz3gS2voCgRzgeKp5kpYZnSJHkOe1NPOiTRPHn1qK6iUg/4Tj/BbaHw/f38rACGI8v8AM7bKo9yamvdTuGkZSfEZ3brlj/uFq41K9jQABATyqTtgdWb2HUmrW2sI9PsSWgUh3cjlM0ndz6IOwqaVg7y7qMA9lA9B7dhV7qCRW6F0hBPKmc5zuSfVid2NWpvrZEVLufvzE+Ch9yPmx6DarvUyzvcOLG2AMk7+UNj8ESbBR6mtLtiGuf2ZbzR2zMQ5VvxnPyj/AFNSSQGFIJI4kw7KVKIue5+vvuaOQyg8pJ834mP/ABWeZ2ZQR6/8etMBkMPMcep/02FFJo3ByUdWOAMgg5qG/toLhAD4kaNsNskU80asBhquraNFkbJFCKLmara1YjnApB3pPWk9aT1qP1qPHWo7XT9H0pd2mlFy49oztTOVUk5PzH6bmmhiMKeXnA8Qj8vZfp607u2e9TcscjghXOI1x1x3p+UoCFGwY9z7HH+1C0Y7BlA86nuPQ4riLUZbcW0It4o2/wAtGAMI38Ug7F/Q9Fq1hZ7q+vGknzzTXDEkJ7KTks59asBbRQRW3gwqfJboSHlb+JO+Tj2Ub+tM6AsoHYKBgD6AbAU6ly7F8dcDAx+gNINgSS24HrTK+ASF/KajtOCLOWW6SRGVjGuMMm+4J71ZXBHIahJG+1R2tmx5+1G7vDJK5wzf2FRCTHK1WMK+ckfU1bXKlogWH1qBFOQR+tWCNgn260DEH8NuWpNW4ytJMgRQ2aIB+pY0PiN8/Mc/qalNy45cksK4i1S7gVLOQK7ABipxWp6lPp8Aj8NbW1YEkVxNdTeHbWzci/8AqtsG96t7OFHv7oyuCDyJgLWmLbP8PZM8jDZnlK1fWKtJcSGBY+gEmd/0FQ2vNyO4Xl3YrvVvJbRgRsAqnnbO59SRUg5uR2IzgMM7igsQYxhwvYHB33zkdPUUGO0fmw2fqN6bRuHtOsjIGcQRmT052UZxRsVJUDFcRX9xL4EWYU2yK1cSrHcKyjqB60QcZoxvEUlQsQc1qUsqRQOSxI6CrkaBC8rsHKb8wq5tryUJP5QemKvF1RGaR+RZFJB9M1a3VlnxcgocL65qSy0cak7nBAUAg718eY3ljLAjKA9cVp0t9baheRq0SOD4Z/FWmRW6JFbRqqgABVAwKiXcRgZ64FAAVsctk0ChzUiwyr4fNkEFa+MdJVjZImAYqTjGTiryG4jSNCVkkZA5GAAKjmjPIckYLe6mpVicRxEKA8fKfmr4W/VJYlyJMt/fGaku7tmLbFsADsBUawS56gCtMuNI5UC8w+f61bfFQ8gGQDTeMuM9RWk+AheME471w+JObwI8/QVp1nZsF5VAFafqWsSqFK8r4znqK0Wcx83ffZq020ELiPcDvWka9w3dWMsSMrgEKduhzitJ0fjrUbL4UrDC2IY/mwuAVrWQ6xwaIFj6rI7Ba1HlUzXUKn8ibgfrTFMZzTlRmtutA4FWl/zMQobp9RVgtskTtlVYlQO2TmrMlRLErlOhqG0duRQANh9DUDKxZAah0/iVliU8w53yRkDmNHxV+tOUOCRtXlvEycls1zXdsvoKMhDcpriex0yJ7GYBsHqCa+0+5vJYDc4KuQcIa4o1rTEe9uHOeYEcpFXOl380kQdQAD8ta9I0bq7hQ4OeWo57WJmkySoqNl8szjPvUVh9pc4lmZFeCN0b2I3q2mBdtQuWU7gLJy7VZWgWFIJQNyruS4JHvReMkEHAp2xivPymRQ2Omd6Yd6bI2o0SooOSAvU0GjzJKsKAFmY9gK+xniG/eKe8u4b+cLDFd5cIW/DgHykVNpWs3dlOoMlrM0bEdDynqPY0k0JIWhZXV3Hy9TQe6tVxuDWIkfHQVYT2kayIpHuK4dgZpFtogx3zgVpdvJ4aNGuO2RWn3ERLhCD/ALVpENs2DGEANavYyBLWUgb4rV3tl8fBI6mrO04vspZY3BSyTzcuVYMxyAa4f1FreLKqDtvXw1nMIJcLLC/gsp2WROn+hr7T9JmcR6et3H1y2cj6GvtQecg6e0S7jypy/wDca421O6hl+IdZlO4MihmzXGaxBp7ieQls8xk7dxUk45ZEZXA3zWwpcCsXBPXArh9NZsNI1s3UcV2S4MQIjYofKkrDcA1o+p6NA2lgW8wkjKOCSCOYU2p8T6rdIfK8uAfXkAXP64oxZBFLb3chK/Oa+NeLkG4Oc1N8KFA6VrkVlCbebFcdxGaNbphvsQla5eXplvLiVjk7nIq8uIlRVkbC1rKStmCQITUks8ZZelKsAHLWkatYlb2wFz4YYx/hYEjsa4s0TV3jGm/CxByU5Yy2V7HLHerrjLStUt78SI1rKgU+7LXFmnJILe38aMEkPF6VxBq2tKtwJ3RT58q2QCf9K4OZ7eGeW5Zw4aRTIUG3staBpgSSxvtTtTknlivZCmD+Eo5YUkSKOcsQOp6msbVzNSeN1BA3rSddtHS4thKyAGMjZ0cdCpFJpPC8iCUFok5Ac5w77AfUdagRMYpA5wK8wIFF9yKiKEctKyBD0FWsysWVTk71aQybIuM1ZqozyjarF4SPKTioVkLBe9IFxikJAxSQ6lFHyDEluhBx3yRUFjwdFOsWJL6eSaRu+AeRaUnbatMuseLAjb5Bx3FaZHKZEiUk9yASKSFQBsewrfrWc0FRmPYU3j3ChuZR09jSxF1EeARuCc5/Wnv76OztVIgt2JJHRpDUxALE14G+DXIOUCm2BU0WOcGiaz1FKOpoDvSv1akNInekA61JcxWU6LkgmH9WOVqPTOH9PtE3EFsiZ9wN6YNtS2ssayjAbYNmk9iKUUS2DWAKVYm33IqaOPUFGC8cReP32yK1nXnSGK3ECRKDcSZyT7L9as7gMTgkEg/pVrjYCrWQbqDWn5P7Ja08naNash0jWrhTipYxmlU4p5zhRV0BmrpW2q+l+WrqIZepr/TbhVXLIPEX/wB0fmFaZdabGyTqXWMB07itHg1XwbTT7u+cAoDCmxYnoM9an1uzWe/txbMyn/KswZkB/PjYN7Vd6bMqFzNbsf2bHcr/ACmkkUUgHTpQht2dGBKkgj/ipU8Ns7S25KqT3P4TTXF5dgksoIxtsParO1jKwQJEGOWCjGTVxYarcLExCl2JH13q8IGWqUjd6lY/Mal75rbcmoMghRUTRsoQ5NPPJkhgPYU8U+6HFfsRyoSauGOfCYGpA/mjNPLFgRjNTwA5TNcSWn2karY6bPc2UAmJREkKZVvMSSK13Qo45JOIraW0urKPxJVlDZlc9B3PKKvbf42S34gtpZ+dTGjyhWfm6g59gK1O0uWtpWyYgCoZwWBA3yPxA1FqVhDOIzGWQEqTTOGEUrIwXIxv+hq5t7TUw/OghClwPyucZB9vSreW0lSaVTEYFdG6cpjqebSb/UWJ5by8keHP8MbVlUpLi8lm/Ngf2qNe1RjqtIv4aUdq6DlpKhY7qKgXooqEHOAKX0pPSlHah6UD2pr+z/6zY25a4t0JnA6yJXG2rXpl02aI2SENI11LyooP/wAqh1RzHq2qQeAQpZreDlnc9eUSOWIWvs33mtLaaO4G6SNM7kH1OTT6BBHYXDr4YyqNnt2zmkMR8OXmZd25SedQP9cZq0FqXnkGHi5hK2CF7Mp9VPcVdarqjafCXAmeWB4QcqCcHKGodO0ays4gAkEKR/2Fftox6mlZ5lPsRSVGKSo6i+4ChS+lJSCk9KWlFQSxOkiqysCGB6EGm4Y+0PWtD0zURlQZORG5lUvuUf8AmAO9fahxBrAs7e5l+FQ4eVVCqMduarm1tIxJJlgu7Fs1zoSxicqpIVupxvtVhYacjSRRqUm5Ww2CF9QOuKn1bU2tLeR5ELFYyTnIk9xTyXUF7IsoSLDhz+N8+UD2A606oebOc0Wu0rVOFOHbnWrO1W6+DaJ7mBtvEg5sScp7MAcg1oPF+hRalpVx4kTbSRnaWB+6Sr2YU3rTU3rRpaWloGgO/wBw9fuNJoVxc6Jws8U98mUn1HIeK3b0i7O4qZtfF1d3EsjvMZJZC+XcvuxJPc0NLtY5LZjsA/hAnAA2rSbu2ieSYLmDLKTgBm7CoYp5xzlkXK8udz6Fa13ibVDIbWRw4OOoHm7invr2NtQYoGfcA+YotWdraQwW8SoqjZVGwrCk4A2rNznHSre8tJ4Jo1kilRkdGGQysMEH61xX9lms213p+oXEVjfSS/BXMMrI48M58J8dWUGuNtKEaavDFq9uNi7YiuB/Wuxr7O+JnjhS/awum6W96BCSfRX+VqH3H1/xNXD3C+kSajrGoxWlsnRnO7t+VFG7N7CuJuK3uNP0R5tL0k5RsHFzcj+dh8q/yD7sEGr65ZoTI+yDJX0B71eTW+PjnCnzLgnrWmeOZZS7kgfMckfr71bwRr4cSYFXDSr4SNkEYbPT61KqhpGy1ARmszMaLsNq03jjgh9HuiEfnE9tN3imXoavdJ1i+sLqIxT2k8kMyEbgxkr/AGNGML5vI2wJ35T6N7VxPwm8VnfmS/01Nmt3bMkS+sLt/wBprgviuNTpmqRvMRlraT9nOv1Q0KHr/gSON3dlVVBZmJwAB3JPQVpGjmax4aSLUbwZDXbf+FiPt3kNcT8V6g2o6zq1xeT74Z/kjH5Y16AfSntfCJbmEicwOMffEnHGmRyorxXBeCRGGQyuvQ1NonLPADLYOchurQ5/C/t6GptQK+DGSG6ntWnafZqjwo8hUB2Izk1ahcxxKv02oxEb0TCa8oNc7KMdTQArwtTs+JrCHa5d4rwAdHbcMfZqjdWVhld1dTQRVVmyB8kncexqa2kjYOUIbKOrEFD7EbitutY+4VwXwTbst3c/EXxXKWMBDS/19kH1rjjjqc2s0wtLBjlbC3JVMesrdXq1eTzAMwzjPQU3xEqEdByrUbqInUFVKggjPasZ5EA5SQR9KkU7qakseI9Jugp/Y3cLkeoDDNSzWoa9AETLtHswZT61pGnQ8un2yQqOiD/ipYnOVIpmTpUqmv8AJnJ3rKKaCDmI3P3afrGlXdhdxCSGdCrD69CPcVf8HcXX1lMh5RO+DjZlbdXFA+X8wGM9KXlwclOhB6qfQ0R92laXp9xe311FbW0CF5ZpG5VUVqmqNPYcMmSxs91a8IxcTD+T+GtTTzM8khLOeZmY8zMT3JNKruwG2eWniuJGQ/KTSXM0DDYj5q5pZwdsv1opKHwfN1z+YdaVs5UUBuoGcbVb67wRw7qkLKVu9Pt5Djs3IFcfow+6KQYdQaQfKf0NSupwmau/ACPyg533qOFVHUj7t68x+lJxJwudUtoS15py5cDq8Hf9Vp7S5kjbYA7U5PMoBOMOPzCkrStG0u6v7+6SC2t4y8sjHoB/uT2Fatx1qrBTJBpdu5Ntaf8A2SermsouPmLCmWVhQVUA9zQWWT6mmVwfUGi3xYHXIxS3EMbY5uceYd8+1FD6qTsa22o6jwfqnD8z5l0qcSwevgXP/CvXmIoj9wrKykAgjBB3BFHh3iW4ltocWV4DLbnGy+qf0mr/AFDVoLCDkNxO4jhV3CB3Y7LltgTX/8QALxEAAgIBAgQFAgYDAQAAAAAAAQIAAxEEEgUhMVETIkFhcRQyEBUgQpGxIzNDUv/aAAgBAgEBPwA2Q2TfN83zdDdUP+i/zG1enXrasS1HGVYETdN8DzfC8LzfC/c4mr4qa+VYB+Y2uuswXcn+o97hfcxLmOM85Vc6MGBxKdSLF7H1E3mbzN8LTfN811p8MIP3dfgS2vJAxk/1Pp+YJloDcs4B6xQqj1ikdZXYVIIMFXEPE3eMCO3pAxwMjnPlgIeI0+8/MqfefmVPYx7d7Bu45R7aqxliBF4jQfWXa0BiRUx98SrVCwjJOe0Sw/EqfMNuorYAkYAlfE6TYU6vCpJzmU8RAqwahn5h4j4V3JQc9Y/Ene5SFlvECKGbbzAzLNXdY+WyZRacchNx2xC6McDOTPH1BHXE0V7mwqTPGsLHdNKVXXEnuYNbpu8retCS1KtnuY2NzMFHxK3zbkVjlDemorapFO7b0xGoZGwVwYiYGNw9/MI96jI6n2OZXtYZBhbAhuvDO1JQbfuBJ3fxDrSR93OJcC+4nrNw55aV6upc5RTK9VXaxHJRLnFb4DAwXKpDBiCPURyt9FZasDOPNEpNBJqIB7kQDUWnFrbhDsUYE3DdLkUKbMAOQRNjrzniN3m8954V0ZbBMWHqDPDcftM0Woxp61forGX2Fh/jzn45Su5mBBGCIxK9ZklpwfRafU1602P56qw657DrPqaHyNkU6foKhFr0+PsEbTlhBw/zA5j6EOfui6RQm3rKKBTkgAjHQy+jbdzuXJ7CO4qsXzlgesFociFQmB6maG569Q4B/wBiFD8dYNJV/wCRPAQdAJ4Mws5TImREBc4AnENIFHKsMzAc8T6UsCiIxPvK9NVpU32HL9puNj7jEJSwN1wYtW+sOhyCMj0MKkHBBmBNom0TAlelLc25CKiKMKMQ1iwhe55GcQOqotZHysNbt5sykYESotNIcAV/xHRWGCIdL2aZlVbWHkPkyuhK/c9/xDEEGaxzqmzYPj2h0RB5HIn09ofkpPKU1bFGesBIOREIsqDj4P4KBkCABVAAwIf0WjFjfp4ex8fb6MDmazUPVaAoHSf/xAAwEQACAgIBAwMBBwMFAAAAAAABAgADBBESEyFBBTFRYRAUICIycZEVQrFSYoGS0f/aAAgBAwEBPwBaoK4EnCcJwgx7j7Vv/wBTFwclvalv41HodG0ykGcJ05wnCCuBJxgQkgATD9HWwbtYj5AiYNFfZEA/zEoQt9BLKlAIHaW0VurKRuX4zVPr3HgzhOBnCBIFnAz0+odRnI/T7fuZXb2J3of5hyPcASklfza2fAh2T4jbl1QdSp8zeKBrgdzj7zfwpg9Ms+RP6XZ8if0yz5Erq6aFfr3ldF1rAKpMb0rIXx/J1Mf0/koDXIP+ZfhmrYAGvn33HrA+stTtOmlnJgO5MbCu47I0vzAyr2lvprNYGFpEuxwaVBJB8SnEtUMCQRML09WyEUntFwaaqvyaEvrBPcwr+aP0nRdsBoToY4JmfTWtHMDWogCr2l7FsCdGz4llbsBxtZZbQGrALHYHuZyNCkFy25g2rVkpYzaXcrtFtew3Jf3lyAsTxbX1QxcI9m5aGv7hqW8kbRE6ZPeY+FhstVeWLdW/oYAcP5MOERa4HdQxAMNB+78dQ43+2Pju2tORGxyieSZXiq3czoV61oTENtDWFLG0Ae3iNmjJ0L0JXt2U6Eusx0QdBOB+girYzbaFd1mfeXWk1c2KKdoD4OoCJsTtD0vBgRDOmnzAib950Gau0J3btMfH4EG4ALrvo99y+itSCp5KfMXT6AnECueqOQ1ArXSsxEOLav8AdDXYB+qHqfJi3KDDm9taiZnHxGyWL8o2QzkbYjuN6Mxqi+HzGNYFAB5EjX/sesOrAAbB8QVdMGG0udj2WZq7xg3+lg0OTb8zrP8AM6p/C7qg2TPSPVcg1BGyrErTekDHUbI4t1GtULLci3JbjWp4b94QK6wojgPWU3ra6jv07GRxog67dxAQR2P4bMlRsL3Md2Y7Yyq0owPjyJ6dVjXVqwAP7z7xUh46l52dgx7QuzMleZLeYGZT2MGT8r9llqIO8e57PoPj7CAZwmO70j8jEb9/rBnBgNjRgyaTWNsAQRL7uZOvaGWrxb7GOlJ+BCxZiTB9olRJrX8OQAaifiY1K2ISSfef/9k=" + } + ] +} diff --git a/examples/react_admin/frontend/src/vite-env.d.ts b/examples/react_admin/frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/react_admin/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/react_admin/frontend/tsconfig.json b/examples/react_admin/frontend/tsconfig.json new file mode 100644 index 0000000000..a273b0cfc0 --- /dev/null +++ b/examples/react_admin/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/examples/react_admin/frontend/vite.config.ts b/examples/react_admin/frontend/vite.config.ts new file mode 100644 index 0000000000..b1e14d1205 --- /dev/null +++ b/examples/react_admin/frontend/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + define: { + 'process.env': process.env, + }, + server: { + host: true, + }, + base: './', +}); diff --git a/examples/rocket_example/.env b/examples/rocket_example/.env new file mode 100644 index 0000000000..6929f13b9a --- /dev/null +++ b/examples/rocket_example/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://root:root@localhost/rocket_example" \ No newline at end of file diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml index 992ad03609..119c09a629 100644 --- a/examples/rocket_example/Cargo.toml +++ b/examples/rocket_example/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "sea-orm-rocket-example" -version = "0.1.0" -authors = ["Sam Samai "] -edition = "2021" -publish = false +authors = ["Sam Samai "] +edition = "2024" +name = "sea-orm-rocket-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] rocket-example-api = { path = "api" } diff --git a/examples/rocket_example/README.md b/examples/rocket_example/README.md index 20845db6a0..9600943e18 100644 --- a/examples/rocket_example/README.md +++ b/examples/rocket_example/README.md @@ -4,15 +4,15 @@ 1. Modify the `url` var in `api/Rocket.toml` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-postgres",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-postgres",` line) 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `🚀 Rocket has launched from http://localhost:8000` line -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock -``` +cd api +cargo test +``` \ No newline at end of file diff --git a/examples/rocket_example/Rocket.toml b/examples/rocket_example/Rocket.toml index 41e0183c92..7514bbd0f7 100644 --- a/examples/rocket_example/Rocket.toml +++ b/examples/rocket_example/Rocket.toml @@ -8,4 +8,8 @@ template_dir = "api/templates/" # Postgres # make sure to enable "sqlx-postgres" feature in Cargo.toml, i.e default = ["sqlx-postgres"] -url = "postgres://root:root@localhost/rocket_example" +# url = "postgres://root:root@localhost/rocket_example" + +# SQLite +# make sure to enable "sqlx-sqlite" feature in Cargo.toml, i.e default = ["sqlx-sqlite"] +url = "sqlite::memory:" diff --git a/examples/rocket_example/api/Cargo.toml b/examples/rocket_example/api/Cargo.toml index 4361c46004..656383a2d4 100644 --- a/examples/rocket_example/api/Cargo.toml +++ b/examples/rocket_example/api/Cargo.toml @@ -1,27 +1,41 @@ [package] -name = "rocket-example-api" -version = "0.1.0" -authors = ["Sam Samai "] -edition = "2021" -publish = false +authors = ["Sam Samai "] +edition = "2024" +name = "rocket-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -async-stream = { version = "0.3" } -async-trait = { version = "0.1" } -rocket-example-core = { path = "../core" } -futures = { version = "0.3" } -futures-util = { version = "0.3" } -rocket = { version = "0.5.0-rc.1", features = [ - "json", -] } -rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ - "tera", -] } -serde_json = { version = "1" } -entity = { path = "../entity" } -migration = { path = "../migration" } -tokio = "1.20.0" +async-stream = { version = "0.3" } +async-trait = { version = "0.1" } +entity = { path = "../entity" } +futures = { version = "0.3" } +futures-util = { version = "0.3" } +migration = { path = "../migration" } +rocket = { version = "0.5", features = ["json"] } +rocket_db_pools = "0.2" +rocket_dyn_templates = { version = "0.2", features = ["tera"] } +serde_json = { version = "1" } +tokio = "1.41" [dependencies.sea-orm-rocket] -path = "../../../sea-orm-rocket/lib" # remove this line in your own project and uncomment the following line -# version = "0.5.2" +path = "../../../sea-orm-rocket/lib" # remove this line in your own project +version = "0.6" + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version + +[dev-dependencies] +rocket-example-api = { path = ".", features = ["sqlite"] } + +[features] +sqlite = ["sea-orm/sqlx-sqlite"] diff --git a/examples/rocket_example/api/src/lib.rs b/examples/rocket_example/api/src/lib.rs index 29eb621d02..8d722fea5a 100644 --- a/examples/rocket_example/api/src/lib.rs +++ b/examples/rocket_example/api/src/lib.rs @@ -3,18 +3,19 @@ extern crate rocket; use rocket::fairing::{self, AdHoc}; use rocket::form::{Context, Form}; -use rocket::fs::{relative, FileServer}; +use rocket::fs::{FileServer, relative}; use rocket::request::FlashMessage; use rocket::response::{Flash, Redirect}; use rocket::{Build, Request, Rocket}; use rocket_dyn_templates::Template; -use rocket_example_core::{Mutation, Query}; use serde_json::json; use migration::MigratorTrait; -use sea_orm_rocket::{Connection, Database}; +use rocket_db_pools::{Connection, Database}; +use service::{Mutation, Query}; mod pool; +pub mod service; use pool::Db; pub use entity::post; @@ -28,12 +29,12 @@ async fn new() -> Template { } #[post("/", data = "")] -async fn create(conn: Connection<'_, Db>, post_form: Form) -> Flash { +async fn create(conn: Connection, post_form: Form) -> Flash { let db = conn.into_inner(); let form = post_form.into_inner(); - Mutation::create_post(db, form) + Mutation::create_post(&db, form) .await .expect("could not insert post"); @@ -41,16 +42,12 @@ async fn create(conn: Connection<'_, Db>, post_form: Form) -> Flash } #[post("/", data = "")] -async fn update( - conn: Connection<'_, Db>, - id: i32, - post_form: Form, -) -> Flash { +async fn update(conn: Connection, id: i32, post_form: Form) -> Flash { let db = conn.into_inner(); let form = post_form.into_inner(); - Mutation::update_post_by_id(db, id, form) + Mutation::update_post_by_id(&db, id, form) .await .expect("could not update post"); @@ -59,7 +56,7 @@ async fn update( #[get("/?&")] async fn list( - conn: Connection<'_, Db>, + conn: Connection, page: Option, posts_per_page: Option, flash: Option>, @@ -73,7 +70,7 @@ async fn list( panic!("Page number cannot be zero"); } - let (posts, num_pages) = Query::find_posts_in_page(db, page, posts_per_page) + let (posts, num_pages) = Query::find_posts_in_page(&db, page, posts_per_page) .await .expect("Cannot find posts in page"); @@ -90,10 +87,10 @@ async fn list( } #[get("/")] -async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { +async fn edit(conn: Connection, id: i32) -> Template { let db = conn.into_inner(); - let post: Option = Query::find_post_by_id(db, id) + let post: Option = Query::find_post_by_id(&db, id) .await .expect("could not find post"); @@ -106,10 +103,10 @@ async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { } #[delete("/")] -async fn delete(conn: Connection<'_, Db>, id: i32) -> Flash { +async fn delete(conn: Connection, id: i32) -> Flash { let db = conn.into_inner(); - Mutation::delete_post(db, id) + Mutation::delete_post(&db, id) .await .expect("could not delete post"); @@ -117,10 +114,10 @@ async fn delete(conn: Connection<'_, Db>, id: i32) -> Flash { } #[delete("/")] -async fn destroy(conn: Connection<'_, Db>) -> Result<(), rocket::response::Debug> { +async fn destroy(conn: Connection) -> Result<(), rocket::response::Debug> { let db = conn.into_inner(); - Mutation::delete_all_posts(db) + Mutation::delete_all_posts(&db) .await .map_err(|e| e.to_string())?; diff --git a/examples/rocket_example/api/src/pool.rs b/examples/rocket_example/api/src/pool.rs index da8470acea..6ed0cc13da 100644 --- a/examples/rocket_example/api/src/pool.rs +++ b/examples/rocket_example/api/src/pool.rs @@ -1,8 +1,7 @@ -use rocket_example_core::sea_orm; - use async_trait::async_trait; +use rocket_db_pools::Database; use sea_orm::ConnectOptions; -use sea_orm_rocket::{rocket::figment::Figment, Config, Database}; +use sea_orm_rocket::{Config, rocket::figment::Figment}; use std::time::Duration; #[derive(Database, Debug)] @@ -15,7 +14,7 @@ pub struct SeaOrmPool { } #[async_trait] -impl sea_orm_rocket::Pool for SeaOrmPool { +impl rocket_db_pools::Pool for SeaOrmPool { type Error = sea_orm::DbErr; type Connection = sea_orm::DatabaseConnection; @@ -36,7 +35,10 @@ impl sea_orm_rocket::Pool for SeaOrmPool { Ok(SeaOrmPool { conn }) } - fn borrow(&self) -> &Self::Connection { - &self.conn + async fn get(&self) -> Result { + Ok(self.conn.clone()) } + + // DatabaseConnection automatically closes on drop + async fn close(&self) {} } diff --git a/examples/rocket_example/api/src/service/mod.rs b/examples/rocket_example/api/src/service/mod.rs new file mode 100644 index 0000000000..4d0b4b4b4e --- /dev/null +++ b/examples/rocket_example/api/src/service/mod.rs @@ -0,0 +1,5 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; diff --git a/examples/salvo_example/core/src/mutation.rs b/examples/rocket_example/api/src/service/mutation.rs similarity index 93% rename from examples/salvo_example/core/src/mutation.rs rename to examples/rocket_example/api/src/service/mutation.rs index dd6891d4ae..f608df16f8 100644 --- a/examples/salvo_example/core/src/mutation.rs +++ b/examples/rocket_example/api/src/service/mutation.rs @@ -1,5 +1,5 @@ -use ::entity::{post, post::Entity as Post}; -use sea_orm::*; +use entity::{post, post::Entity as Post}; +use sea_orm::{DbConn, DbErr, DeleteResult, entity::*}; pub struct Mutation; diff --git a/examples/tonic_example/core/src/query.rs b/examples/rocket_example/api/src/service/query.rs similarity index 86% rename from examples/tonic_example/core/src/query.rs rename to examples/rocket_example/api/src/service/query.rs index e8d2668f56..4a2f01f379 100644 --- a/examples/tonic_example/core/src/query.rs +++ b/examples/rocket_example/api/src/service/query.rs @@ -1,5 +1,5 @@ -use ::entity::{post, post::Entity as Post}; -use sea_orm::*; +use entity::{post, post::Entity as Post}; +use sea_orm::{DbConn, DbErr, EntityTrait, PaginatorTrait, QueryOrder}; pub struct Query; diff --git a/examples/rocket_example/api/tests/crud_tests.rs b/examples/rocket_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..728a980d6d --- /dev/null +++ b/examples/rocket_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use entity::post; +use rocket_example_api::service::{Mutation, Query}; +use sea_orm::Database; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/rocket_example/core/src/lib.rs b/examples/rocket_example/core/src/lib.rs deleted file mode 100644 index 4a80f23913..0000000000 --- a/examples/rocket_example/core/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod mutation; -mod query; - -pub use mutation::*; -pub use query::*; - -pub use sea_orm; diff --git a/examples/rocket_example/core/tests/mock.rs b/examples/rocket_example/core/tests/mock.rs deleted file mode 100644 index 84b187e5c1..0000000000 --- a/examples/rocket_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::post; -use prepare::prepare_mock_db; -use rocket_example_core::{Mutation, Query}; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/rocket_example/core/tests/prepare.rs b/examples/rocket_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/rocket_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/rocket_example/entity/Cargo.toml b/examples/rocket_example/entity/Cargo.toml index 666471bb3c..3a4144494d 100644 --- a/examples/rocket_example/entity/Cargo.toml +++ b/examples/rocket_example/entity/Cargo.toml @@ -1,18 +1,17 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" path = "src/lib.rs" [dependencies] -rocket = { version = "0.5.0-rc.1", features = [ - "json", -] } +rocket = { version = "0.5", features = ["json"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/rocket_example/entity/src/post.rs b/examples/rocket_example/entity/src/post.rs index ecf3f98f2d..7391ab7c66 100644 --- a/examples/rocket_example/entity/src/post.rs +++ b/examples/rocket_example/entity/src/post.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize, FromForm)] #[serde(crate = "rocket::serde")] -#[sea_orm(table_name = "posts")] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, diff --git a/examples/rocket_example/migration/Cargo.toml b/examples/rocket_example/migration/Cargo.toml index 63672d5b1a..f5c9543ea2 100644 --- a/examples/rocket_example/migration/Cargo.toml +++ b/examples/rocket_example/migration/Cargo.toml @@ -1,22 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -rocket = { version = "0.5.0-rc.1" } -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-postgres", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-postgres", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/rocket_example/migration/src/lib.rs b/examples/rocket_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/rocket_example/migration/src/lib.rs +++ b/examples/rocket_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs b/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/rocket_example/migration/src/m20220120_000002_seed_posts.rs b/examples/rocket_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/rocket_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/rocket_example/migration/src/main.rs b/examples/rocket_example/migration/src/main.rs index 4626e82f7f..f054deaf88 100644 --- a/examples/rocket_example/migration/src/main.rs +++ b/examples/rocket_example/migration/src/main.rs @@ -1,17 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { - // Setting `DATABASE_URL` environment variable - let key = "DATABASE_URL"; - if std::env::var(key).is_err() { - // Getting the database URL from Rocket.toml if it's not set - let figment = rocket::Config::figment(); - let database_url: String = figment - .extract_inner("databases.sea_orm.url") - .expect("Cannot find Database URL in Rocket.toml"); - std::env::set_var(key, database_url); - } - cli::run_cli(migration::Migrator).await; } diff --git a/examples/rocket_okapi_example/Cargo.toml b/examples/rocket_okapi_example/Cargo.toml index 87dfde6f37..c8aa9f321d 100644 --- a/examples/rocket_okapi_example/Cargo.toml +++ b/examples/rocket_okapi_example/Cargo.toml @@ -1,12 +1,16 @@ [package] +authors = [ + "Sam Samai ", + "Erick Pacheco ", "Erick Pacheco "] -edition = "2021" -publish = false +authors = ["Sam Samai "] +edition = "2024" +name = "rocket-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] async-stream = { version = "0.3" } async-trait = { version = "0.1" } -rocket-example-core = { path = "../core" } +dto = { path = "../dto" } +entity = { path = "../entity" } futures = { version = "0.3" } futures-util = { version = "0.3" } -rocket = { version = "0.5.0-rc.1", features = [ - "json", -] } -rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ - "tera", -] } -serde_json = { version = "1" } -entity = { path = "../entity" } migration = { path = "../migration" } -tokio = "1.20.0" +rocket = { version = "0.5", features = ["json"] } +rocket-okapi-example-service = { path = "../service" } +rocket_cors = "0.6" +rocket_db_pools = "0.2" +rocket_dyn_templates = { version = "0.2", features = ["tera"] } +rocket_okapi = { version = "0.9", features = [ + "swagger", + "rapidoc", + "rocket_db_pools", +] } serde = "1.0" -dto = { path = "../dto" } +serde_json = { version = "1" } +tokio = "1.41" [dependencies.sea-orm-rocket] +features = [ + "rocket_okapi", +] # enables rocket_okapi so to have open api features enabled path = "../../../sea-orm-rocket/lib" # remove this line in your own project and use the version line -features = ["rocket_okapi"] # enables rocket_okapi so to have open api features enabled -# version = "0.5.1" - -[dependencies.rocket_okapi] -version = "0.8.0-rc.2" -features = ["swagger", "rapidoc","rocket_db_pools"] - -[dependencies.rocket_cors] -git = "https://github.com/lawliet89/rocket_cors.git" -rev = "54fae070" -default-features = false \ No newline at end of file +# version = "0.5" diff --git a/examples/rocket_okapi_example/api/src/error.rs b/examples/rocket_okapi_example/api/src/error.rs index 88a27472aa..ed0a964049 100644 --- a/examples/rocket_okapi_example/api/src/error.rs +++ b/examples/rocket_okapi_example/api/src/error.rs @@ -5,7 +5,7 @@ use rocket::{ }; use rocket_okapi::okapi::openapi3::Responses; use rocket_okapi::okapi::schemars::{self, Map}; -use rocket_okapi::{gen::OpenApiGenerator, response::OpenApiResponderInner, OpenApiError}; +use rocket_okapi::{OpenApiError, r#gen::OpenApiGenerator, response::OpenApiResponderInner}; /// Error messages returned to user #[derive(Debug, serde::Serialize, schemars::JsonSchema)] @@ -21,12 +21,12 @@ pub struct Error { impl OpenApiResponderInner for Error { fn responses(_generator: &mut OpenApiGenerator) -> Result { - use rocket_okapi::okapi::openapi3::{RefOr, Response as OpenApiReponse}; + use rocket_okapi::okapi::openapi3::{RefOr, Response as OpenApiResponse}; let mut responses = Map::new(); responses.insert( "400".to_string(), - RefOr::Object(OpenApiReponse { + RefOr::Object(OpenApiResponse { description: "\ # [400 Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)\n\ The request given is wrongly formatted or data asked could not be fulfilled. \ @@ -37,7 +37,7 @@ impl OpenApiResponderInner for Error { ); responses.insert( "404".to_string(), - RefOr::Object(OpenApiReponse { + RefOr::Object(OpenApiResponse { description: "\ # [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404)\n\ This response is given when you request a page that does not exists.\ @@ -48,7 +48,7 @@ impl OpenApiResponderInner for Error { ); responses.insert( "422".to_string(), - RefOr::Object(OpenApiReponse { + RefOr::Object(OpenApiResponse { description: "\ # [422 Unprocessable Entity](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422)\n\ This response is given when you request body is not correctly formatted. \ @@ -58,7 +58,7 @@ impl OpenApiResponderInner for Error { ); responses.insert( "500".to_string(), - RefOr::Object(OpenApiReponse { + RefOr::Object(OpenApiResponse { description: "\ # [500 Internal Server Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)\n\ This response is given when something wend wrong on the server. \ diff --git a/examples/rocket_okapi_example/api/src/lib.rs b/examples/rocket_okapi_example/api/src/lib.rs index 1f82656b90..802091704f 100644 --- a/examples/rocket_okapi_example/api/src/lib.rs +++ b/examples/rocket_okapi_example/api/src/lib.rs @@ -5,13 +5,13 @@ use rocket::fairing::{self, AdHoc}; use rocket::{Build, Rocket}; use migration::MigratorTrait; -use sea_orm_rocket::Database; +use rocket_db_pools::Database; use rocket_okapi::mount_endpoints_and_merged_docs; use rocket_okapi::okapi::openapi3::OpenApi; -use rocket_okapi::rapidoc::{make_rapidoc, GeneralConfig, HideShowConfig, RapiDocConfig}; +use rocket_okapi::rapidoc::{GeneralConfig, HideShowConfig, RapiDocConfig, make_rapidoc}; use rocket_okapi::settings::UrlObject; -use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; +use rocket_okapi::swagger_ui::{SwaggerUIConfig, make_swagger_ui}; use rocket::http::Method; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; diff --git a/examples/rocket_okapi_example/api/src/okapi_example.rs b/examples/rocket_okapi_example/api/src/okapi_example.rs index 69d15d1812..9b7be1e191 100644 --- a/examples/rocket_okapi_example/api/src/okapi_example.rs +++ b/examples/rocket_okapi_example/api/src/okapi_example.rs @@ -1,8 +1,8 @@ use dto::dto; use rocket::serde::json::Json; -use rocket_example_core::{Mutation, Query}; +use rocket_okapi_example_service::{Mutation, Query}; -use sea_orm_rocket::Connection; +use rocket_db_pools::Connection; use rocket_okapi::okapi::openapi3::OpenApi; @@ -11,7 +11,6 @@ use crate::pool; use pool::Db; pub use entity::post; -pub use entity::post::Entity as Post; use rocket_okapi::settings::OpenApiSettings; @@ -30,13 +29,10 @@ pub type DataResult<'a, T> = /// # Add a new post #[openapi(tag = "POST")] #[post("/", data = "")] -async fn create( - conn: Connection<'_, Db>, - post_data: DataResult<'_, post::Model>, -) -> R> { +async fn create(conn: Connection, post_data: DataResult<'_, post::Model>) -> R> { let db = conn.into_inner(); let form = post_data?.into_inner(); - let cmd = Mutation::create_post(db, form); + let cmd = Mutation::create_post(&db, form); match cmd.await { Ok(_) => Ok(Json(Some("Post successfully added.".to_string()))), Err(e) => { @@ -54,7 +50,7 @@ async fn create( #[openapi(tag = "POST")] #[post("/", data = "")] async fn update( - conn: Connection<'_, Db>, + conn: Connection, id: i32, post_data: DataResult<'_, post::Model>, ) -> R> { @@ -62,7 +58,7 @@ async fn update( let form = post_data?.into_inner(); - let cmd = Mutation::update_post_by_id(db, id, form); + let cmd = Mutation::update_post_by_id(&db, id, form); match cmd.await { Ok(_) => Ok(Json(Some("Post successfully updated.".to_string()))), Err(e) => { @@ -80,7 +76,7 @@ async fn update( #[openapi(tag = "POST")] #[get("/?&")] async fn list( - conn: Connection<'_, Db>, + conn: Connection, page: Option, posts_per_page: Option, ) -> R { @@ -98,7 +94,7 @@ async fn list( return Err(m); } - let (posts, num_pages) = Query::find_posts_in_page(db, page, posts_per_page) + let (posts, num_pages) = Query::find_posts_in_page(&db, page, posts_per_page) .await .expect("Cannot find posts in page"); @@ -113,10 +109,10 @@ async fn list( /// # get post by Id #[openapi(tag = "POST")] #[get("/")] -async fn get_by_id(conn: Connection<'_, Db>, id: i32) -> R> { +async fn get_by_id(conn: Connection, id: i32) -> R> { let db = conn.into_inner(); - let post: Option = Query::find_post_by_id(db, id) + let post: Option = Query::find_post_by_id(&db, id) .await .expect("could not find post"); Ok(Json(post)) @@ -125,10 +121,10 @@ async fn get_by_id(conn: Connection<'_, Db>, id: i32) -> R> /// # delete post by Id #[openapi(tag = "POST")] #[delete("/")] -async fn delete(conn: Connection<'_, Db>, id: i32) -> R> { +async fn delete(conn: Connection, id: i32) -> R> { let db = conn.into_inner(); - let cmd = Mutation::delete_post(db, id); + let cmd = Mutation::delete_post(&db, id); match cmd.await { Ok(_) => Ok(Json(Some("Post successfully deleted.".to_string()))), Err(e) => { @@ -145,10 +141,10 @@ async fn delete(conn: Connection<'_, Db>, id: i32) -> R> { /// # delete all posts #[openapi(tag = "POST")] #[delete("/")] -async fn destroy(conn: Connection<'_, Db>) -> R> { +async fn destroy(conn: Connection) -> R> { let db = conn.into_inner(); - let cmd = Mutation::delete_all_posts(db); + let cmd = Mutation::delete_all_posts(&db); match cmd.await { Ok(_) => Ok(Json(Some( diff --git a/examples/rocket_okapi_example/api/src/pool.rs b/examples/rocket_okapi_example/api/src/pool.rs index b1c0567794..88355e2071 100644 --- a/examples/rocket_okapi_example/api/src/pool.rs +++ b/examples/rocket_okapi_example/api/src/pool.rs @@ -1,8 +1,9 @@ -use rocket_example_core::sea_orm; +use rocket_okapi_example_service::sea_orm; use async_trait::async_trait; +use rocket_db_pools::Database; use sea_orm::ConnectOptions; -use sea_orm_rocket::{rocket::figment::Figment, Config, Database}; +use sea_orm_rocket::{Config, rocket::figment::Figment}; use std::time::Duration; #[derive(Database, Debug)] @@ -15,7 +16,7 @@ pub struct SeaOrmPool { } #[async_trait] -impl sea_orm_rocket::Pool for SeaOrmPool { +impl rocket_db_pools::Pool for SeaOrmPool { type Error = sea_orm::DbErr; type Connection = sea_orm::DatabaseConnection; @@ -35,7 +36,10 @@ impl sea_orm_rocket::Pool for SeaOrmPool { Ok(SeaOrmPool { conn }) } - fn borrow(&self) -> &Self::Connection { - &self.conn + async fn get(&self) -> Result { + Ok(self.conn.clone()) } + + // DatabaseConnection automatically closes on drop + async fn close(&self) {} } diff --git a/examples/rocket_okapi_example/core/Cargo.toml b/examples/rocket_okapi_example/core/Cargo.toml deleted file mode 100644 index 6ee1abf942..0000000000 --- a/examples/rocket_okapi_example/core/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "rocket-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "runtime-tokio-native-tls", - "sqlx-postgres", - # "sqlx-mysql", - # "sqlx-sqlite", -] - -[dev-dependencies] -tokio = "1.20.0" - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/rocket_okapi_example/core/src/lib.rs b/examples/rocket_okapi_example/core/src/lib.rs deleted file mode 100644 index 4a80f23913..0000000000 --- a/examples/rocket_okapi_example/core/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod mutation; -mod query; - -pub use mutation::*; -pub use query::*; - -pub use sea_orm; diff --git a/examples/rocket_okapi_example/core/tests/mock.rs b/examples/rocket_okapi_example/core/tests/mock.rs deleted file mode 100644 index 84b187e5c1..0000000000 --- a/examples/rocket_okapi_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::post; -use prepare::prepare_mock_db; -use rocket_example_core::{Mutation, Query}; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/rocket_okapi_example/core/tests/prepare.rs b/examples/rocket_okapi_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/rocket_okapi_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/rocket_okapi_example/dto/Cargo.toml b/examples/rocket_okapi_example/dto/Cargo.toml index a0f208dbaf..4ad6dc28a3 100644 --- a/examples/rocket_okapi_example/dto/Cargo.toml +++ b/examples/rocket_okapi_example/dto/Cargo.toml @@ -1,20 +1,19 @@ [package] -name = "dto" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "dto" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "dto" path = "src/lib.rs" [dependencies] -rocket = { version = "0.5.0-rc.1", features = [ - "json", -] } +rocket = { version = "0.5", features = ["json"] } [dependencies.entity] path = "../entity" [dependencies.rocket_okapi] -version = "0.8.0-rc.2" \ No newline at end of file +version = "0.9" diff --git a/examples/rocket_okapi_example/entity/Cargo.toml b/examples/rocket_okapi_example/entity/Cargo.toml index ea666d840c..83005ba411 100644 --- a/examples/rocket_okapi_example/entity/Cargo.toml +++ b/examples/rocket_okapi_example/entity/Cargo.toml @@ -1,21 +1,20 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" path = "src/lib.rs" [dependencies] -rocket = { version = "0.5.0-rc.1", features = [ - "json", -] } +rocket = { version = "0.5", features = ["json"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version [dependencies.rocket_okapi] -version = "0.8.0-rc.2" +version = "0.9" diff --git a/examples/rocket_okapi_example/entity/src/post.rs b/examples/rocket_okapi_example/entity/src/post.rs index a5797f484e..97bd39e8a7 100644 --- a/examples/rocket_okapi_example/entity/src/post.rs +++ b/examples/rocket_okapi_example/entity/src/post.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize, FromForm, JsonSchema, )] #[serde(crate = "rocket::serde")] -#[sea_orm(table_name = "posts")] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, diff --git a/examples/rocket_okapi_example/migration/Cargo.toml b/examples/rocket_okapi_example/migration/Cargo.toml index 63672d5b1a..c829fb1ae1 100644 --- a/examples/rocket_okapi_example/migration/Cargo.toml +++ b/examples/rocket_okapi_example/migration/Cargo.toml @@ -1,22 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -rocket = { version = "0.5.0-rc.1" } -async-std = { version = "1", features = ["attributes", "tokio1"] } +rocket = { version = "0.5" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-postgres", + # Enable following runtime and db backend features if you want to run migration via CLI + # "runtime-tokio-native-tls", + # "sqlx-postgres", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs b/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/rocket_okapi_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/rocket_okapi_example/migration/src/main.rs b/examples/rocket_okapi_example/migration/src/main.rs index 4626e82f7f..600d6dfafd 100644 --- a/examples/rocket_okapi_example/migration/src/main.rs +++ b/examples/rocket_okapi_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { // Setting `DATABASE_URL` environment variable let key = "DATABASE_URL"; @@ -10,7 +10,9 @@ async fn main() { let database_url: String = figment .extract_inner("databases.sea_orm.url") .expect("Cannot find Database URL in Rocket.toml"); - std::env::set_var(key, database_url); + unsafe { + std::env::set_var(key, database_url); + } } cli::run_cli(migration::Migrator).await; diff --git a/examples/rocket_example/core/Cargo.toml b/examples/rocket_okapi_example/service/Cargo.toml similarity index 53% rename from examples/rocket_example/core/Cargo.toml rename to examples/rocket_okapi_example/service/Cargo.toml index 6ee1abf942..d828760720 100644 --- a/examples/rocket_example/core/Cargo.toml +++ b/examples/rocket_okapi_example/service/Cargo.toml @@ -1,7 +1,8 @@ [package] -name = "rocket-example-core" -version = "0.1.0" -edition = "2021" +edition = "2024" +name = "rocket-okapi-example-service" +rust-version = "1.85.0" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,21 +10,14 @@ edition = "2021" entity = { path = "../entity" } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version features = [ "runtime-tokio-native-tls", - "sqlx-postgres", - # "sqlx-mysql", - # "sqlx-sqlite", + # "sqlx-postgres", + # "sqlx-mysql", + "sqlx-sqlite", ] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version [dev-dependencies] -tokio = "1.20.0" - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] +tokio = "1.41" diff --git a/examples/actix3_example/core/src/lib.rs b/examples/rocket_okapi_example/service/src/lib.rs similarity index 100% rename from examples/actix3_example/core/src/lib.rs rename to examples/rocket_okapi_example/service/src/lib.rs diff --git a/examples/poem_example/core/src/mutation.rs b/examples/rocket_okapi_example/service/src/mutation.rs similarity index 100% rename from examples/poem_example/core/src/mutation.rs rename to examples/rocket_okapi_example/service/src/mutation.rs diff --git a/examples/poem_example/core/src/query.rs b/examples/rocket_okapi_example/service/src/query.rs similarity index 100% rename from examples/poem_example/core/src/query.rs rename to examples/rocket_okapi_example/service/src/query.rs diff --git a/examples/rocket_okapi_example/service/tests/crud_tests.rs b/examples/rocket_okapi_example/service/tests/crud_tests.rs new file mode 100644 index 0000000000..ad3ce2d447 --- /dev/null +++ b/examples/rocket_okapi_example/service/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use entity::post; +use rocket_okapi_example_service::{Mutation, Query}; +use sea_orm::Database; + +#[tokio::test] +async fn main() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/salvo_example/.env b/examples/salvo_example/.env index acd83c0e1f..65d16de114 100644 --- a/examples/salvo_example/.env +++ b/examples/salvo_example/.env @@ -1,4 +1,4 @@ HOST=127.0.0.1 PORT=8000 -#DATABASE_URL="mysql://root:@localhost/poem_example" -DATABASE_URL="sqlite::memory:" +RUST_LOG=debug +DATABASE_URL="sqlite://posts.db?mode=rwc" diff --git a/examples/salvo_example/Cargo.toml b/examples/salvo_example/Cargo.toml index aef0228449..befaf17bb2 100644 --- a/examples/salvo_example/Cargo.toml +++ b/examples/salvo_example/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "sea-orm-salvo-example" -version = "0.1.0" -edition = "2021" +edition = "2024" +name = "sea-orm-salvo-example" +rust-version = "1.85.0" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] salvo-example-api = { path = "api" } diff --git a/examples/salvo_example/README.md b/examples/salvo_example/README.md index d0aa6973b7..3bbb970702 100644 --- a/examples/salvo_example/README.md +++ b/examples/salvo_example/README.md @@ -1,18 +1,18 @@ ![screenshot](Screenshot.png) -# Poem with SeaORM example app +# Salvo with SeaORM example app 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `api/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock -``` +cd api +cargo test +``` \ No newline at end of file diff --git a/examples/salvo_example/api/Cargo.toml b/examples/salvo_example/api/Cargo.toml index 0c2003ba86..2f02323528 100644 --- a/examples/salvo_example/api/Cargo.toml +++ b/examples/salvo_example/api/Cargo.toml @@ -1,15 +1,26 @@ [package] -name = "salvo-example-api" -version = "0.1.0" -edition = "2021" +edition = "2024" +name = "salvo-example-api" +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -salvo-example-core = { path = "../core" } -tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } -salvo = { version = "0.27", features = ["affix", "serve-static"] } +dotenvy = "0.15" +entity = { path = "../entity" } +migration = { path = "../migration" } +salvo = { version = "0.50", features = ["affix", "serve-static"] } +serde = { version = "1", features = ["derive"] } +tera = "1.19.0" +tokio = { version = "1.29.0", features = ["macros", "rt-multi-thread"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -serde = { version = "1", features = ["derive"] } -tera = "1.8.0" -dotenvy = "0.15" -entity = { path = "../entity" } -migration = { path = "../migration" } + +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-native-tls", + # "sqlx-mysql", + # "sqlx-postgres", + "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/salvo_example/api/src/lib.rs b/examples/salvo_example/api/src/lib.rs index 3b7da0ba73..af90cfe229 100644 --- a/examples/salvo_example/api/src/lib.rs +++ b/examples/salvo_example/api/src/lib.rs @@ -1,15 +1,11 @@ -use std::env; +pub mod service; use entity::post; use migration::{Migrator, MigratorTrait}; -use salvo::extra::affix; -use salvo::extra::serve_static::DirHandler; +use salvo::affix; use salvo::prelude::*; -use salvo::writer::Text; -use salvo_example_core::{ - sea_orm::{Database, DatabaseConnection}, - Mutation, Query, -}; +use sea_orm::{Database, DatabaseConnection}; +use service::{Mutation, Query}; use tera::Tera; const DEFAULT_POSTS_PER_PAGE: u64 = 5; @@ -29,7 +25,7 @@ async fn create(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res let conn = &state.conn; let form = req - .extract_form::() + .parse_form::() .await .map_err(|_| StatusError::bad_request())?; @@ -37,7 +33,7 @@ async fn create(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res .await .map_err(|_| StatusError::internal_server_error())?; - res.redirect_found("/"); + Redirect::found("/").render(res); Ok(()) } @@ -114,7 +110,7 @@ async fn update(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res let conn = &state.conn; let id = req.param::("id").unwrap_or_default(); let form = req - .extract_form::() + .parse_form::() .await .map_err(|_| StatusError::bad_request())?; @@ -122,7 +118,7 @@ async fn update(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res .await .map_err(|_| StatusError::internal_server_error())?; - res.redirect_found("/"); + Redirect::found("/").render(res); Ok(()) } @@ -138,20 +134,19 @@ async fn delete(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Res .await .map_err(|_| StatusError::internal_server_error())?; - res.redirect_found("/"); + Redirect::found("/").render(res); Ok(()) } #[tokio::main] pub async fn main() { - std::env::set_var("RUST_LOG", "debug"); tracing_subscriber::fmt::init(); // get env vars dotenvy::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); + let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = std::env::var("HOST").expect("HOST is not set in .env file"); + let port = std::env::var("PORT").expect("PORT is not set in .env file"); let server_url = format!("{host}:{port}"); // create post table if not exists @@ -170,13 +165,13 @@ pub async fn main() { .push(Router::with_path("").get(edit).post(update)) .push(Router::with_path("delete/").post(delete)) .push( - Router::with_path("static/<**>").get(DirHandler::new(concat!( + Router::with_path("static/<**>").get(salvo::prelude::StaticDir::new(concat!( env!("CARGO_MANIFEST_DIR"), "/static" ))), ); - Server::new(TcpListener::bind(&format!("{host}:{port}"))) + Server::new(TcpListener::bind(TcpListener::new(format!("{host}:{port}"))).await) .serve(router) .await; } diff --git a/examples/salvo_example/api/src/service/mod.rs b/examples/salvo_example/api/src/service/mod.rs new file mode 100644 index 0000000000..4d0b4b4b4e --- /dev/null +++ b/examples/salvo_example/api/src/service/mod.rs @@ -0,0 +1,5 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; diff --git a/examples/rocket_example/core/src/mutation.rs b/examples/salvo_example/api/src/service/mutation.rs similarity index 100% rename from examples/rocket_example/core/src/mutation.rs rename to examples/salvo_example/api/src/service/mutation.rs diff --git a/examples/rocket_example/core/src/query.rs b/examples/salvo_example/api/src/service/query.rs similarity index 100% rename from examples/rocket_example/core/src/query.rs rename to examples/salvo_example/api/src/service/query.rs diff --git a/examples/salvo_example/api/templates/layout.html.tera b/examples/salvo_example/api/templates/layout.html.tera index 50ae58856c..abebef2818 100644 --- a/examples/salvo_example/api/templates/layout.html.tera +++ b/examples/salvo_example/api/templates/layout.html.tera @@ -2,7 +2,7 @@ - Poem Example + Salvo Example diff --git a/examples/salvo_example/api/tests/crud_tests.rs b/examples/salvo_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..0a62604b58 --- /dev/null +++ b/examples/salvo_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use entity::post; +use salvo_example_api::service::{Mutation, Query}; +use sea_orm::Database; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/salvo_example/core/Cargo.toml b/examples/salvo_example/core/Cargo.toml deleted file mode 100644 index a6e33269aa..0000000000 --- a/examples/salvo_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "salvo-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - # "sqlx-mysql", - # "sqlx-postgres", - "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/salvo_example/core/src/lib.rs b/examples/salvo_example/core/src/lib.rs deleted file mode 100644 index 4a80f23913..0000000000 --- a/examples/salvo_example/core/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod mutation; -mod query; - -pub use mutation::*; -pub use query::*; - -pub use sea_orm; diff --git a/examples/salvo_example/core/src/query.rs b/examples/salvo_example/core/src/query.rs deleted file mode 100644 index e8d2668f56..0000000000 --- a/examples/salvo_example/core/src/query.rs +++ /dev/null @@ -1,26 +0,0 @@ -use ::entity::{post, post::Entity as Post}; -use sea_orm::*; - -pub struct Query; - -impl Query { - pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { - Post::find_by_id(id).one(db).await - } - - /// If ok, returns (post models, num pages). - pub async fn find_posts_in_page( - db: &DbConn, - page: u64, - posts_per_page: u64, - ) -> Result<(Vec, u64), DbErr> { - // Setup paginator - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(db, posts_per_page); - let num_pages = paginator.num_pages().await?; - - // Fetch paginated posts - paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) - } -} diff --git a/examples/salvo_example/core/tests/mock.rs b/examples/salvo_example/core/tests/mock.rs deleted file mode 100644 index 261652bfee..0000000000 --- a/examples/salvo_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::post; -use prepare::prepare_mock_db; -use salvo_example_core::{Mutation, Query}; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/salvo_example/core/tests/prepare.rs b/examples/salvo_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/salvo_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/salvo_example/entity/Cargo.toml b/examples/salvo_example/entity/Cargo.toml index 7dea367337..9e429a350b 100644 --- a/examples/salvo_example/entity/Cargo.toml +++ b/examples/salvo_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,5 +13,5 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/salvo_example/entity/src/lib.rs b/examples/salvo_example/entity/src/lib.rs index e8b6291ac3..b85eeb2ad5 100644 --- a/examples/salvo_example/entity/src/lib.rs +++ b/examples/salvo_example/entity/src/lib.rs @@ -1 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub mod prelude; + pub mod post; diff --git a/examples/salvo_example/entity/src/post.rs b/examples/salvo_example/entity/src/post.rs index 62cde830cf..2e8ff73bbd 100644 --- a/examples/salvo_example/entity/src/post.rs +++ b/examples/salvo_example/entity/src/post.rs @@ -1,8 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] -#[sea_orm(table_name = "posts")] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] @@ -12,7 +15,4 @@ pub struct Model { pub text: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/salvo_example/entity/src/prelude.rs b/examples/salvo_example/entity/src/prelude.rs new file mode 100644 index 0000000000..0840a758e9 --- /dev/null +++ b/examples/salvo_example/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub use super::post::Entity as Post; diff --git a/examples/salvo_example/migration/Cargo.toml b/examples/salvo_example/migration/Cargo.toml index 37cd12fc79..9b7dfb86c7 100644 --- a/examples/salvo_example/migration/Cargo.toml +++ b/examples/salvo_example/migration/Cargo.toml @@ -1,21 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-native-tls", - # "sqlx-sqlite", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-sqlite", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/salvo_example/migration/src/lib.rs b/examples/salvo_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/salvo_example/migration/src/lib.rs +++ b/examples/salvo_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/salvo_example/migration/src/m20220120_000001_create_post_table.rs b/examples/salvo_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/salvo_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/salvo_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/salvo_example/migration/src/m20220120_000002_seed_posts.rs b/examples/salvo_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/salvo_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/salvo_example/migration/src/main.rs b/examples/salvo_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/salvo_example/migration/src/main.rs +++ b/examples/salvo_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/seaography_example/README.md b/examples/seaography_example/README.md new file mode 100644 index 0000000000..9e512dc6b9 --- /dev/null +++ b/examples/seaography_example/README.md @@ -0,0 +1,115 @@ +# SeaORM + Seaography Example + +| ![](https://raw.githubusercontent.com/SeaQL/sea-orm/master/examples/seaography_example/Seaography%20example.png) | +|:--:| +| Seaography screenshot with Bakery schema | + +| ![](https://raw.githubusercontent.com/SeaQL/sea-orm/master/tests/common/bakery_chain/bakery_chain_erd.png) | +|:--:| +| The Bakery schema | + +## Running the project + +Specify a database url + +```sh +export DATABASE_URL="sqlite://../bakery.db" +``` + +Then, run + +```sh +cd graphql +cargo run +``` + +## Run some queries + +### Find chocolate cakes and know where to buy them + +```graphql +{ + cake(filters: { name: { contains: "Chocolate" } }) { + nodes { + name + price + bakery { + name + } + } + } +} +``` + +### Find all cakes baked by Alice + +```graphql +{ + cake(having: { baker: { name: { eq: "Alice" } } }) { + nodes { + name + price + baker { + nodes { + name + } + } + } + } +} +``` + +### Bakery -> Cake -> Baker + +```graphql +{ + bakery(pagination: { page: { limit: 10, page: 0 } }, orderBy: { name: ASC }) { + nodes { + name + cake { + nodes { + name + price + baker { + nodes { + name + } + } + } + } + } + } +} +``` + +## Starting from scratch + +### Setup the Database + +`cd` into `migration` folder and follow instructions there, but basically: + +```sh +export DATABASE_URL="sqlite://../bakery.db?mode=rwc" +``` + +```sh +cd migration +cargo run +``` + +### Install Seaography + +```sh +cargo install sea-orm-cli@^2.0.0-rc +cargo install seaography-cli@^2.0.0-rc +``` + +### Generate GraphQL project + +```sh +rm -rf graphql # this entire folder is generated +mkdir graphql +cd graphql +sea-orm-cli generate entity --output-dir ./src/entities --entity-format dense --seaography +seaography-cli -o . -e ./src/entities --framework axum sea-orm-seaography-example +``` diff --git a/examples/seaography_example/Seaography example.png b/examples/seaography_example/Seaography example.png new file mode 100644 index 0000000000..a13d4a6ac3 Binary files /dev/null and b/examples/seaography_example/Seaography example.png differ diff --git a/examples/seaography_example/bakery.db b/examples/seaography_example/bakery.db new file mode 100644 index 0000000000..3581019fd3 Binary files /dev/null and b/examples/seaography_example/bakery.db differ diff --git a/examples/seaography_example/graphql/.env b/examples/seaography_example/graphql/.env new file mode 100644 index 0000000000..dd677762b9 --- /dev/null +++ b/examples/seaography_example/graphql/.env @@ -0,0 +1,3 @@ +DATABASE_URL="sqlite://../bakery.db" +# COMPLEXITY_LIMIT= +# DEPTH_LIMIT= \ No newline at end of file diff --git a/examples/seaography_example/graphql/Cargo.lock b/examples/seaography_example/graphql/Cargo.lock new file mode 100644 index 0000000000..376d98fecc --- /dev/null +++ b/examples/seaography_example/graphql/Cargo.lock @@ -0,0 +1,3609 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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 = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-graphql" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64", + "bytes", + "chrono", + "fnv", + "futures-channel", + "futures-timer", + "futures-util", + "http", + "indexmap", + "lru", + "mime", + "multer", + "num-traits", + "pin-project-lite", + "regex", + "rust_decimal", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-axum" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725874ecfbf399e071150b8619c4071d7b2b7a2f117e173dddef53c6bdb6bb1" +dependencies = [ + "async-graphql", + "axum", + "bytes", + "futures-util", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", +] + +[[package]] +name = "async-graphql-derive" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling 0.20.11", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum 0.26.3", + "syn 2.0.107", + "thiserror 1.0.69", +] + +[[package]] +name = "async-graphql-parser" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" +dependencies = [ + "bytes", + "indexmap", + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[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 = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.107", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.107", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags", + "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.107", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +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.107", + "version_check", + "yansi", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c0efc234bdf1f073cc9b448da21a22ed0ac7ff0958a0b3bc4994041dc059df" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more", + "futures-util", + "itertools 0.14.0", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum 0.27.2", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b6ce0ae263925930d4e0f95e24a5a8b069dccc61a1ef68da26338470d94929" +dependencies = [ + "heck 0.5.0", + "pluralizer", + "proc-macro-crate", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.107", + "unicode-ident", +] + +[[package]] +name = "sea-orm-seaography-example" +version = "0.1.0" +dependencies = [ + "async-graphql-axum", + "axum", + "dotenv", + "sea-orm", + "seaography", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93948054fb2d208555a96d03d2c887591deb42ffe3210eedbd8a3234c6fb6d34" +dependencies = [ + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365d236217f5daa4f40d3c9998ff3921351b53472da50308e384388162353b3a" +dependencies = [ + "darling 0.20.11", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.107", + "thiserror 2.0.17", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68873fa1776b4c25a26e7679f8ee22332978c721168ec1b0b32b6583d5a9381d" +dependencies = [ + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b363dd21c20fe4d1488819cb2bc7f8d4696c62dd9f39554f97639f54d57dd0ab" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "seaography" +version = "2.0.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11cae5906dc5dbd23c732023be91b1f62b273be9c7c20b1de8a3223a3fcc12b3" +dependencies = [ + "async-graphql", + "fnv", + "heck 0.4.1", + "itertools 0.12.1", + "lazy_static", + "sea-orm", + "seaography-macros", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "seaography-macros" +version = "2.0.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a0c2bc62d0562d777c4e76fdf73659ce72dad21bc128ded9961eaf2e39b902" +dependencies = [ + "darling 0.21.3", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "native-tls", + "once_cell", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.107", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.107", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.107", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[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.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] diff --git a/examples/seaography_example/graphql/Cargo.toml b/examples/seaography_example/graphql/Cargo.toml new file mode 100644 index 0000000000..6ce606efce --- /dev/null +++ b/examples/seaography_example/graphql/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition = "2021" +name = "sea-orm-seaography-example" +version = "0.1.0" + +[dependencies] +async-graphql-axum = { version = "7.0" } +axum = { version = "0.8" } +dotenv = "0.15.0" +tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } +tracing = { version = "0.1.37" } +tracing-subscriber = { version = "0.3.17" } + +[dependencies.sea-orm] +features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] +version = "~2.0.0-rc" + +[dependencies.seaography] +features = ["graphql-playground", "with-decimal", "with-chrono"] +version = "~2.0.0-rc.3" # seaography version + +[dev-dependencies] +serde_json = { version = "1.0.103" } + +[workspace] +members = [] diff --git a/examples/seaography_example/graphql/src/entities/baker.rs b/examples/seaography_example/graphql/src/entities/baker.rs new file mode 100644 index 0000000000..0bc36253a5 --- /dev/null +++ b/examples/seaography_example/graphql/src/entities/baker.rs @@ -0,0 +1,26 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "baker")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub contact: String, + pub bakery_id: Option, + #[sea_orm( + belongs_to, + from = "bakery_id", + to = "id", + on_update = "Cascade", + on_delete = "Cascade" + )] + pub bakery: HasOne, + #[sea_orm(has_many, via = "cake_baker")] + pub cakes: HasMany, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/graphql/src/entities/bakery.rs b/examples/seaography_example/graphql/src/entities/bakery.rs new file mode 100644 index 0000000000..6c50e65cf1 --- /dev/null +++ b/examples/seaography_example/graphql/src/entities/bakery.rs @@ -0,0 +1,20 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "bakery")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(column_type = "Double")] + pub profit_margin: f64, + #[sea_orm(has_many)] + pub bakers: HasMany, + #[sea_orm(has_many)] + pub cakes: HasMany, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/graphql/src/entities/cake.rs b/examples/seaography_example/graphql/src/entities/cake.rs new file mode 100644 index 0000000000..07d333d0fd --- /dev/null +++ b/examples/seaography_example/graphql/src/entities/cake.rs @@ -0,0 +1,28 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(column_type = "Decimal(Some((16, 4)))")] + pub price: Decimal, + pub bakery_id: i32, + pub gluten_free: bool, + #[sea_orm( + belongs_to, + from = "bakery_id", + to = "id", + on_update = "Cascade", + on_delete = "Cascade" + )] + pub bakery: HasOne, + #[sea_orm(has_many, via = "cake_baker")] + pub bakers: HasMany, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/graphql/src/entities/cake_baker.rs b/examples/seaography_example/graphql/src/entities/cake_baker.rs new file mode 100644 index 0000000000..a8d846394b --- /dev/null +++ b/examples/seaography_example/graphql/src/entities/cake_baker.rs @@ -0,0 +1,31 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_baker")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub baker_id: i32, + #[sea_orm( + belongs_to, + from = "baker_id", + to = "id", + on_update = "Cascade", + on_delete = "Cascade" + )] + pub baker: HasOne, + #[sea_orm( + belongs_to, + from = "cake_id", + to = "id", + on_update = "Cascade", + on_delete = "Cascade" + )] + pub cake: HasOne, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/graphql/src/entities/mod.rs b/examples/seaography_example/graphql/src/entities/mod.rs new file mode 100644 index 0000000000..831ef94dc9 --- /dev/null +++ b/examples/seaography_example/graphql/src/entities/mod.rs @@ -0,0 +1,10 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + +pub mod prelude; + +pub mod baker; +pub mod bakery; +pub mod cake; +pub mod cake_baker; + +seaography::register_entity_modules!([baker, bakery, cake, cake_baker,]); diff --git a/examples/seaography_example/graphql/src/entities/prelude.rs b/examples/seaography_example/graphql/src/entities/prelude.rs new file mode 100644 index 0000000000..51819e4846 --- /dev/null +++ b/examples/seaography_example/graphql/src/entities/prelude.rs @@ -0,0 +1,6 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; +pub use super::cake::Entity as Cake; +pub use super::cake_baker::Entity as CakeBaker; diff --git a/examples/seaography_example/graphql/src/lib.rs b/examples/seaography_example/graphql/src/lib.rs new file mode 100644 index 0000000000..a292add5da --- /dev/null +++ b/examples/seaography_example/graphql/src/lib.rs @@ -0,0 +1,2 @@ +pub mod entities; +pub mod query_root; diff --git a/examples/seaography_example/graphql/src/main.rs b/examples/seaography_example/graphql/src/main.rs new file mode 100644 index 0000000000..0ea7dc2ad0 --- /dev/null +++ b/examples/seaography_example/graphql/src/main.rs @@ -0,0 +1,61 @@ +use async_graphql::{ + dynamic::Schema, + http::{playground_source, GraphQLPlaygroundConfig}, +}; +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use axum::{ + extract::State, + response::{self, IntoResponse}, + routing::get, + Router, +}; +use dotenv::dotenv; +use sea_orm::Database; +use seaography::{async_graphql, lazy_static::lazy_static}; +use std::env; +use tokio::net::TcpListener; + +lazy_static! { + static ref URL: String = env::var("URL").unwrap_or("localhost:8000".into()); + static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); + static ref DATABASE_URL: String = + env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); + static ref DEPTH_LIMIT: Option = env::var("DEPTH_LIMIT").map_or(None, |data| Some( + data.parse().expect("DEPTH_LIMIT is not a number") + )); + static ref COMPLEXITY_LIMIT: Option = env::var("COMPLEXITY_LIMIT") + .map_or(None, |data| { + Some(data.parse().expect("COMPLEXITY_LIMIT is not a number")) + }); +} + +async fn graphql_playground() -> impl IntoResponse { + response::Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) +} + +async fn graphql_handler(State(schema): State, req: GraphQLRequest) -> GraphQLResponse { + let req = req.into_inner(); + schema.execute(req).await.into() +} + +#[tokio::main] +async fn main() { + dotenv().ok(); + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .init(); + let db = Database::connect(&*DATABASE_URL) + .await + .expect("Fail to initialize database connection"); + let schema = + sea_orm_seaography_example::query_root::schema(db, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) + .unwrap(); + let app = Router::new() + .route(&*ENDPOINT, get(graphql_playground).post(graphql_handler)) + .with_state(schema); + println!("Visit GraphQL Playground at http://{}", *URL); + axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) + .await + .unwrap(); +} diff --git a/examples/seaography_example/graphql/src/query_root.rs b/examples/seaography_example/graphql/src/query_root.rs new file mode 100644 index 0000000000..42178a2bbf --- /dev/null +++ b/examples/seaography_example/graphql/src/query_root.rs @@ -0,0 +1,31 @@ +use crate::entities::*; +use async_graphql::dynamic::*; +use sea_orm::DatabaseConnection; +use seaography::{async_graphql, lazy_static::lazy_static, Builder, BuilderContext}; + +lazy_static! { + static ref CONTEXT: BuilderContext = BuilderContext::default(); +} + +pub fn schema( + database: DatabaseConnection, + depth: Option, + complexity: Option, +) -> Result { + schema_builder(&CONTEXT, database, depth, complexity).finish() +} + +pub fn schema_builder( + context: &'static BuilderContext, + database: DatabaseConnection, + depth: Option, + complexity: Option, +) -> SchemaBuilder { + let mut builder = Builder::new(context, database.clone()); + builder = register_entity_modules(builder); + builder + .set_depth_limit(depth) + .set_complexity_limit(complexity) + .schema_builder() + .data(database) +} diff --git a/examples/seaography_example/graphql/tests/query_tests.rs b/examples/seaography_example/graphql/tests/query_tests.rs new file mode 100644 index 0000000000..2aa134b56e --- /dev/null +++ b/examples/seaography_example/graphql/tests/query_tests.rs @@ -0,0 +1,266 @@ +use async_graphql::{dynamic::*, Response}; +use sea_orm::Database; +use seaography::async_graphql; + +async fn schema() -> Schema { + let database = Database::connect( + std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://../bakery.db".into()), + ) + .await + .unwrap(); + sea_orm_seaography_example::query_root::schema(database, None, None).unwrap() +} + +fn assert_eq(a: Response, b: &str) { + assert_eq!( + a.data.into_json().unwrap(), + serde_json::from_str::(b).unwrap() + ) +} + +#[tokio::test] +async fn test_cake_with_bakery() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + cake(filters: { name: { contains: "Chocolate" } }) { + nodes { + name + price + bakery { + name + } + } + } + } + "#, + ) + .await, + r#" + { + "cake": { + "nodes": [ + { + "name": "Chocolate Cake", + "price": "10.25", + "bakery": { + "name": "SeaSide Bakery" + } + }, + { + "name": "Double Chocolate", + "price": "12.5", + "bakery": { + "name": "SeaSide Bakery" + } + }, + { + "name": "Double Chocolate", + "price": "12.5", + "bakery": { + "name": "LakeSide Bakery" + } + } + ] + } + } + "#, + ) +} + +#[tokio::test] +async fn test_cake_with_baker() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + cake( + filters: { name: { contains: "Cheese" } } + having: { baker: { name: { eq: "Alice" } } } + ) { + nodes { + name + price + baker { + nodes { + name + } + } + } + } + } + "#, + ) + .await, + r#" + { + "cake": { + "nodes": [ + { + "name": "New York Cheese", + "price": "12.5", + "baker": { + "nodes": [ + { + "name": "Alice" + }, + { + "name": "Bob" + } + ] + } + }, + { + "name": "New York Cheese", + "price": "12.5", + "baker": { + "nodes": [ + { + "name": "Alice" + }, + { + "name": "Bob" + } + ] + } + }, + { + "name": "Blueburry Cheese", + "price": "11.5", + "baker": { + "nodes": [ + { + "name": "Alice" + } + ] + } + } + ] + } + } + "#, + ) +} + +#[tokio::test] +async fn test_bakery_with_cake_with_baker() { + let schema = schema().await; + + assert_eq( + schema + .execute( + r#" + { + bakery(pagination: { page: { limit: 1, page: 0 } }, orderBy: { name: ASC }) { + nodes { + name + cake { + nodes { + name + price + baker { + nodes { + name + } + } + } + } + } + } + } + "#, + ) + .await, + r#" + { + "bakery": { + "nodes": [ + { + "name": "LakeSide Bakery", + "cake": { + "nodes": [ + { + "name": "Double Chocolate", + "price": "12.5", + "baker": { + "nodes": [ + { + "name": "Bob" + } + ] + } + }, + { + "name": "Lemon Cake", + "price": "8.8", + "baker": { + "nodes": [ + { + "name": "Bob" + } + ] + } + }, + { + "name": "Strawberry Cake", + "price": "9.9", + "baker": { + "nodes": [ + { + "name": "Bob" + } + ] + } + }, + { + "name": "Orange Cake", + "price": "6.5", + "baker": { + "nodes": [ + { + "name": "Bob" + } + ] + } + }, + { + "name": "New York Cheese", + "price": "12.5", + "baker": { + "nodes": [ + { + "name": "Alice" + }, + { + "name": "Bob" + } + ] + } + }, + { + "name": "Blueburry Cheese", + "price": "11.5", + "baker": { + "nodes": [ + { + "name": "Bob" + } + ] + } + } + ] + } + } + ] + } + } + "#, + ) +} diff --git a/examples/seaography_example/migration/Cargo.toml b/examples/seaography_example/migration/Cargo.toml new file mode 100644 index 0000000000..6b1574c923 --- /dev/null +++ b/examples/seaography_example/migration/Cargo.toml @@ -0,0 +1,27 @@ +[workspace] + +[package] +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version + +[dependencies.sea-orm-migration] +features = [ + "runtime-tokio-native-tls", + "sqlx-sqlite", +] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/seaography_example/migration/README.md b/examples/seaography_example/migration/README.md new file mode 100644 index 0000000000..7751f6c88f --- /dev/null +++ b/examples/seaography_example/migration/README.md @@ -0,0 +1,60 @@ +# Bakery Schema + +## MySQL + +Assume the database is named `bakery`: + +```sql +CREATE DATABASE bakery; +GRANT ALL PRIVILEGES ON bakery.* TO sea; +``` + +## SQLite + +```sh +export DATABASE_URL=sqlite://../bakery.db?mode=rwc +``` + +# Re-generate entities + +```sh +sea-orm-cli generate entity --output-dir src/entity +``` + +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/seaography_example/migration/src/entity/baker.rs b/examples/seaography_example/migration/src/entity/baker.rs new file mode 100644 index 0000000000..31f2367f05 --- /dev/null +++ b/examples/seaography_example/migration/src/entity/baker.rs @@ -0,0 +1,50 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "baker")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub contact: String, + pub bakery_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::bakery::Entity", + from = "Column::BakeryId", + to = "super::bakery::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Bakery, + #[sea_orm(has_many = "super::cake_baker::Entity")] + CakeBaker, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bakery.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeBaker.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::cake_baker::Relation::Cake.def() + } + fn via() -> Option { + Some(super::cake_baker::Relation::Baker.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/migration/src/entity/bakery.rs b/examples/seaography_example/migration/src/entity/bakery.rs new file mode 100644 index 0000000000..82186837aa --- /dev/null +++ b/examples/seaography_example/migration/src/entity/bakery.rs @@ -0,0 +1,35 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "bakery")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(column_type = "Double")] + pub profit_margin: f64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::baker::Entity")] + Baker, + #[sea_orm(has_many = "super::cake::Entity")] + Cake, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Baker.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/migration/src/entity/cake.rs b/examples/seaography_example/migration/src/entity/cake.rs new file mode 100644 index 0000000000..0283e74717 --- /dev/null +++ b/examples/seaography_example/migration/src/entity/cake.rs @@ -0,0 +1,52 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(column_type = "Decimal(Some((16, 4)))")] + pub price: Decimal, + pub bakery_id: Option, + pub gluten_free: i8, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::bakery::Entity", + from = "Column::BakeryId", + to = "super::bakery::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Bakery, + #[sea_orm(has_many = "super::cake_baker::Entity")] + CakeBaker, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bakery.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeBaker.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::cake_baker::Relation::Baker.def() + } + fn via() -> Option { + Some(super::cake_baker::Relation::Cake.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/migration/src/entity/cake_baker.rs b/examples/seaography_example/migration/src/entity/cake_baker.rs new file mode 100644 index 0000000000..28107aaf92 --- /dev/null +++ b/examples/seaography_example/migration/src/entity/cake_baker.rs @@ -0,0 +1,46 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_baker")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub baker_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::baker::Entity", + from = "Column::BakerId", + to = "super::baker::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Baker, + #[sea_orm( + belongs_to = "super::cake::Entity", + from = "Column::CakeId", + to = "super::cake::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Cake, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Baker.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/seaography_example/migration/src/entity/mod.rs b/examples/seaography_example/migration/src/entity/mod.rs new file mode 100644 index 0000000000..dc59aec47f --- /dev/null +++ b/examples/seaography_example/migration/src/entity/mod.rs @@ -0,0 +1,8 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1 + +pub mod prelude; + +pub mod baker; +pub mod bakery; +pub mod cake; +pub mod cake_baker; diff --git a/examples/seaography_example/migration/src/entity/prelude.rs b/examples/seaography_example/migration/src/entity/prelude.rs new file mode 100644 index 0000000000..9433bfe6a8 --- /dev/null +++ b/examples/seaography_example/migration/src/entity/prelude.rs @@ -0,0 +1,6 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; +pub use super::cake::Entity as Cake; +pub use super::cake_baker::Entity as CakeBaker; diff --git a/examples/seaography_example/migration/src/lib.rs b/examples/seaography_example/migration/src/lib.rs new file mode 100644 index 0000000000..cdc6fe14b0 --- /dev/null +++ b/examples/seaography_example/migration/src/lib.rs @@ -0,0 +1,29 @@ +pub use sea_orm_migration::prelude::*; + +mod entity; +mod m20230101_000001_create_bakery_table; +mod m20230101_000002_create_baker_table; +mod m20230101_000003_create_cake_table; +mod m20230101_000004_create_cake_baker_table; +mod m20230101_000005_create_customer_table; +mod m20230101_000006_create_order_table; +mod m20230101_000007_create_lineitem_table; +mod m20230102_000001_seed_bakery_data; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20230101_000001_create_bakery_table::Migration), + Box::new(m20230101_000002_create_baker_table::Migration), + Box::new(m20230101_000003_create_cake_table::Migration), + Box::new(m20230101_000004_create_cake_baker_table::Migration), + // Box::new(m20230101_000005_create_customer_table::Migration), + // Box::new(m20230101_000006_create_order_table::Migration), + // Box::new(m20230101_000007_create_lineitem_table::Migration), + Box::new(m20230102_000001_seed_bakery_data::Migration), + ] + } +} diff --git a/examples/seaography_example/migration/src/m20230101_000001_create_bakery_table.rs b/examples/seaography_example/migration/src/m20230101_000001_create_bakery_table.rs new file mode 100644 index 0000000000..f1ce264c78 --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000001_create_bakery_table.rs @@ -0,0 +1,26 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table("bakery") + .col(pk_auto("id")) + .col(string("name")) + .col(double("profit_margin")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("bakery").to_owned()) + .await + } +} diff --git a/examples/seaography_example/migration/src/m20230101_000002_create_baker_table.rs b/examples/seaography_example/migration/src/m20230101_000002_create_baker_table.rs new file mode 100644 index 0000000000..0677a120cb --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000002_create_baker_table.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table("baker") + .col(pk_auto("id")) + .col(string("name")) + .col(string("contact")) + .col(integer_null("bakery_id")) + .foreign_key( + ForeignKey::create() + .name("fk-baker-bakery_id") + .from("baker", "bakery_id") + .to("bakery", "id") + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("baker").to_owned()) + .await + } +} diff --git a/examples/seaography_example/migration/src/m20230101_000003_create_cake_table.rs b/examples/seaography_example/migration/src/m20230101_000003_create_cake_table.rs new file mode 100644 index 0000000000..a6baf5591b --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000003_create_cake_table.rs @@ -0,0 +1,36 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table("cake") + .col(pk_auto("id")) + .col(string("name")) + .col(decimal_len("price", 16, 4)) + .col(integer("bakery_id")) + .foreign_key( + ForeignKey::create() + .name("fk-cake-bakery_id") + .from("cake", "bakery_id") + .to("bakery", "id") + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .col(boolean("gluten_free")) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("cake").to_owned()) + .await + } +} diff --git a/examples/seaography_example/migration/src/m20230101_000004_create_cake_baker_table.rs b/examples/seaography_example/migration/src/m20230101_000004_create_cake_baker_table.rs new file mode 100644 index 0000000000..8156160adf --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000004_create_cake_baker_table.rs @@ -0,0 +1,47 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table("cake_baker") + .col(integer("cake_id")) + .col(integer("baker_id")) + .primary_key( + Index::create() + .name("pk-cake_baker") + .col("cake_id") + .col("baker_id"), + ) + .foreign_key( + ForeignKey::create() + .name("fk-cake_baker-cake_id") + .from("cake_baker", "cake_id") + .to("cake", "id") + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-cake_baker-baker_id") + .from("cake_baker", "baker_id") + .to("baker", "id") + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("cake_baker").to_owned()) + .await + } +} diff --git a/examples/seaography_example/migration/src/m20230101_000005_create_customer_table.rs b/examples/seaography_example/migration/src/m20230101_000005_create_customer_table.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000005_create_customer_table.rs @@ -0,0 +1 @@ + diff --git a/examples/seaography_example/migration/src/m20230101_000006_create_order_table.rs b/examples/seaography_example/migration/src/m20230101_000006_create_order_table.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000006_create_order_table.rs @@ -0,0 +1 @@ + diff --git a/examples/seaography_example/migration/src/m20230101_000007_create_lineitem_table.rs b/examples/seaography_example/migration/src/m20230101_000007_create_lineitem_table.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/examples/seaography_example/migration/src/m20230101_000007_create_lineitem_table.rs @@ -0,0 +1 @@ + diff --git a/examples/seaography_example/migration/src/m20230102_000001_seed_bakery_data.rs b/examples/seaography_example/migration/src/m20230102_000001_seed_bakery_data.rs new file mode 100644 index 0000000000..4a9a2091b5 --- /dev/null +++ b/examples/seaography_example/migration/src/m20230102_000001_seed_bakery_data.rs @@ -0,0 +1,205 @@ +use crate::entity::{prelude::*, *}; +use sea_orm::entity::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let bakery = bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + profit_margin: Set(10.4), + ..Default::default() + }; + let sea = Bakery::insert(bakery).exec(db).await?.last_insert_id; + + let bakery = bakery::ActiveModel { + name: Set("LakeSide Bakery".to_owned()), + profit_margin: Set(5.8), + ..Default::default() + }; + let lake = Bakery::insert(bakery).exec(db).await?.last_insert_id; + + let alice = baker::ActiveModel { + name: Set("Alice".to_owned()), + contact: Set("+44 15273388".to_owned()), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let alice = Baker::insert(alice).exec(db).await?.last_insert_id; + + let bob = baker::ActiveModel { + name: Set("Bob".to_owned()), + contact: Set("+852 12345678".to_owned()), + bakery_id: Set(Some(lake)), + ..Default::default() + }; + let bob = Baker::insert(bob).exec(db).await?.last_insert_id; + + let cake = cake::ActiveModel { + name: Set("Chocolate Cake".to_owned()), + price: Set("10.25".parse().unwrap()), + gluten_free: Set(0), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let choco = Cake::insert(cake).exec(db).await?.last_insert_id; + + let mut cake = cake::ActiveModel { + name: Set("Double Chocolate".to_owned()), + price: Set("12.5".parse().unwrap()), + gluten_free: Set(0), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let double_1 = Cake::insert(cake.clone()).exec(db).await?.last_insert_id; + cake.bakery_id = Set(Some(lake)); + let double_2 = Cake::insert(cake).exec(db).await?.last_insert_id; + + let mut cake = cake::ActiveModel { + name: Set("Lemon Cake".to_owned()), + price: Set("8.8".parse().unwrap()), + gluten_free: Set(0), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let lemon_1 = Cake::insert(cake.clone()).exec(db).await?.last_insert_id; + cake.bakery_id = Set(Some(lake)); + let lemon_2 = Cake::insert(cake).exec(db).await?.last_insert_id; + + let mut cake = cake::ActiveModel { + name: Set("Strawberry Cake".to_owned()), + price: Set("9.9".parse().unwrap()), + gluten_free: Set(0), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let straw_1 = Cake::insert(cake.clone()).exec(db).await?.last_insert_id; + cake.bakery_id = Set(Some(lake)); + let straw_2 = Cake::insert(cake).exec(db).await?.last_insert_id; + + let cake = cake::ActiveModel { + name: Set("Orange Cake".to_owned()), + price: Set("6.5".parse().unwrap()), + gluten_free: Set(1), + bakery_id: Set(Some(lake)), + ..Default::default() + }; + let orange = Cake::insert(cake).exec(db).await?.last_insert_id; + + let mut cake = cake::ActiveModel { + name: Set("New York Cheese".to_owned()), + price: Set("12.5".parse().unwrap()), + gluten_free: Set(0), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let cheese_1 = Cake::insert(cake.clone()).exec(db).await?.last_insert_id; + cake.bakery_id = Set(Some(lake)); + let cheese_2 = Cake::insert(cake).exec(db).await?.last_insert_id; + + let mut cake = cake::ActiveModel { + name: Set("Blueburry Cheese".to_owned()), + price: Set("11.5".parse().unwrap()), + gluten_free: Set(1), + bakery_id: Set(Some(sea)), + ..Default::default() + }; + let blue_1 = Cake::insert(cake.clone()).exec(db).await?.last_insert_id; + cake.bakery_id = Set(Some(lake)); + let blue_2 = Cake::insert(cake).exec(db).await?.last_insert_id; + + let rel = cake_baker::ActiveModel { + cake_id: Set(choco), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + + let rel = cake_baker::ActiveModel { + cake_id: Set(double_1), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(double_2), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + + let rel = cake_baker::ActiveModel { + cake_id: Set(lemon_1), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(lemon_2), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + + let rel = cake_baker::ActiveModel { + cake_id: Set(straw_1), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(straw_2), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + + let rel = cake_baker::ActiveModel { + cake_id: Set(orange), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + + let rel = cake_baker::ActiveModel { + cake_id: Set(cheese_1), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(cheese_1), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(cheese_2), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(cheese_2), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + + let rel = cake_baker::ActiveModel { + cake_id: Set(blue_1), + baker_id: Set(alice), + }; + CakeBaker::insert(rel).exec(db).await?; + let rel = cake_baker::ActiveModel { + cake_id: Set(blue_2), + baker_id: Set(bob), + }; + CakeBaker::insert(rel).exec(db).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + Cake::delete_many().exec(db).await?; + Baker::delete_many().exec(db).await?; + Bakery::delete_many().exec(db).await?; + + Ok(()) + } +} diff --git a/examples/seaography_example/migration/src/main.rs b/examples/seaography_example/migration/src/main.rs new file mode 100644 index 0000000000..f054deaf88 --- /dev/null +++ b/examples/seaography_example/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[tokio::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/examples/tonic_example/.env b/examples/tonic_example/.env new file mode 100644 index 0000000000..b5b4ace085 --- /dev/null +++ b/examples/tonic_example/.env @@ -0,0 +1 @@ +DATABASE_URL="sqlite://posts.db?mode=rwc" diff --git a/examples/tonic_example/Cargo.toml b/examples/tonic_example/Cargo.toml index d3d147048f..f2233dac1d 100644 --- a/examples/tonic_example/Cargo.toml +++ b/examples/tonic_example/Cargo.toml @@ -1,18 +1,19 @@ [package] -name = "sea-orm-tonic-example" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-tonic-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "api", "core", "entity", "migration"] +members = [".", "api", "entity", "migration"] [dependencies] +tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "full"] } +tonic = "0.9.2" tonic-example-api = { path = "api" } -tonic = "0.7" -tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "full"] } [[bin]] name = "server" diff --git a/examples/tonic_example/README.md b/examples/tonic_example/README.md index 7c11feace5..ad4bbfd5e6 100644 --- a/examples/tonic_example/README.md +++ b/examples/tonic_example/README.md @@ -14,9 +14,9 @@ run client using cargo run --bin client ``` -Run mock test on the core logic crate: +Run tests: ```bash -cd core -cargo test --features mock +cd api +cargo test ``` diff --git a/examples/tonic_example/api/Cargo.toml b/examples/tonic_example/api/Cargo.toml index 6253c53116..32c82ff2ac 100644 --- a/examples/tonic_example/api/Cargo.toml +++ b/examples/tonic_example/api/Cargo.toml @@ -1,20 +1,28 @@ [package] -name = "tonic-example-api" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "tonic-example-api" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -tonic-example-core = { path = "../core" } -tonic = "0.7" -tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "full"] } -entity = { path = "../entity" } +entity = { path = "../entity" } migration = { path = "../migration" } -prost = "0.10.0" -serde = "1.0" +prost = "0.11.9" +serde = "1.0" +tokio = { version = "1.29", features = ["full"] } +tonic = "0.9.2" -[lib] -path = "./src/lib.rs" +[dependencies.sea-orm] +features = [ + "debug-print", + "runtime-tokio-rustls", + # "sqlx-mysql", + # "sqlx-postgres", + "sqlx-sqlite", +] +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version [build-dependencies] -tonic-build = "0.7" +tonic-build = "0.9.2" diff --git a/examples/tonic_example/api/src/lib.rs b/examples/tonic_example/api/src/lib.rs index 32d97981b7..702971f943 100644 --- a/examples/tonic_example/api/src/lib.rs +++ b/examples/tonic_example/api/src/lib.rs @@ -1,12 +1,12 @@ +pub mod service; + use tonic::transport::Server; use tonic::{Request, Response, Status}; use entity::post; use migration::{Migrator, MigratorTrait}; -use tonic_example_core::{ - sea_orm::{Database, DatabaseConnection}, - Mutation, Query, -}; +use sea_orm::{Database, DatabaseConnection}; +use service::{Mutation, Query}; use std::env; @@ -15,8 +15,8 @@ pub mod post_mod { } use post_mod::{ - blogpost_server::{Blogpost, BlogpostServer}, Post, PostId, PostList, PostPerPage, ProcessStatus, + blogpost_server::{Blogpost, BlogpostServer}, }; impl Post { diff --git a/examples/tonic_example/api/src/service/mod.rs b/examples/tonic_example/api/src/service/mod.rs new file mode 100644 index 0000000000..4d0b4b4b4e --- /dev/null +++ b/examples/tonic_example/api/src/service/mod.rs @@ -0,0 +1,5 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; diff --git a/examples/rocket_okapi_example/core/src/mutation.rs b/examples/tonic_example/api/src/service/mutation.rs similarity index 100% rename from examples/rocket_okapi_example/core/src/mutation.rs rename to examples/tonic_example/api/src/service/mutation.rs diff --git a/examples/rocket_okapi_example/core/src/query.rs b/examples/tonic_example/api/src/service/query.rs similarity index 100% rename from examples/rocket_okapi_example/core/src/query.rs rename to examples/tonic_example/api/src/service/query.rs diff --git a/examples/tonic_example/api/tests/crud_tests.rs b/examples/tonic_example/api/tests/crud_tests.rs new file mode 100644 index 0000000000..91ba68f269 --- /dev/null +++ b/examples/tonic_example/api/tests/crud_tests.rs @@ -0,0 +1,105 @@ +use entity::post; +use sea_orm::Database; +use tonic_example_api::service::{Mutation, Query}; + +#[tokio::test] +async fn crud_tests() { + let db = &Database::connect("sqlite::memory:").await.unwrap(); + + db.get_schema_builder() + .register(post::Entity) + .apply(db) + .await + .unwrap(); + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(1), + title: sea_orm::ActiveValue::Unchanged("Title A".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text A".to_owned()) + } + ); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title B".to_owned(), + text: "Text B".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(2), + title: sea_orm::ActiveValue::Unchanged("Title B".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text B".to_owned()) + } + ); + } + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + assert_eq!(post.title, "Title A"); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 2).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let post = Query::find_post_by_id(db, 2).await.unwrap(); + assert!(post.is_none()); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } +} diff --git a/examples/tonic_example/core/Cargo.toml b/examples/tonic_example/core/Cargo.toml deleted file mode 100644 index 91dbcd1644..0000000000 --- a/examples/tonic_example/core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "tonic-example-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -entity = { path = "../entity" } - -[dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-rustls", - # "sqlx-mysql", - "sqlx-postgres", - # "sqlx-sqlite", -] - -[dev-dependencies] -tokio = { version = "1.20.0", features = ["macros", "rt"] } - -[features] -mock = ["sea-orm/mock"] - -[[test]] -name = "mock" -required-features = ["mock"] diff --git a/examples/tonic_example/core/src/lib.rs b/examples/tonic_example/core/src/lib.rs deleted file mode 100644 index 4a80f23913..0000000000 --- a/examples/tonic_example/core/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod mutation; -mod query; - -pub use mutation::*; -pub use query::*; - -pub use sea_orm; diff --git a/examples/tonic_example/core/src/mutation.rs b/examples/tonic_example/core/src/mutation.rs deleted file mode 100644 index dd6891d4ae..0000000000 --- a/examples/tonic_example/core/src/mutation.rs +++ /dev/null @@ -1,53 +0,0 @@ -use ::entity::{post, post::Entity as Post}; -use sea_orm::*; - -pub struct Mutation; - -impl Mutation { - pub async fn create_post( - db: &DbConn, - form_data: post::Model, - ) -> Result { - post::ActiveModel { - title: Set(form_data.title.to_owned()), - text: Set(form_data.text.to_owned()), - ..Default::default() - } - .save(db) - .await - } - - pub async fn update_post_by_id( - db: &DbConn, - id: i32, - form_data: post::Model, - ) -> Result { - let post: post::ActiveModel = Post::find_by_id(id) - .one(db) - .await? - .ok_or(DbErr::Custom("Cannot find post.".to_owned())) - .map(Into::into)?; - - post::ActiveModel { - id: post.id, - title: Set(form_data.title.to_owned()), - text: Set(form_data.text.to_owned()), - } - .update(db) - .await - } - - pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = Post::find_by_id(id) - .one(db) - .await? - .ok_or(DbErr::Custom("Cannot find post.".to_owned())) - .map(Into::into)?; - - post.delete(db).await - } - - pub async fn delete_all_posts(db: &DbConn) -> Result { - Post::delete_many().exec(db).await - } -} diff --git a/examples/tonic_example/core/tests/mock.rs b/examples/tonic_example/core/tests/mock.rs deleted file mode 100644 index 522d3e4521..0000000000 --- a/examples/tonic_example/core/tests/mock.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod prepare; - -use entity::post; -use prepare::prepare_mock_db; -use tonic_example_core::{Mutation, Query}; - -#[tokio::test] -async fn main() { - let db = &prepare_mock_db(); - - { - let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); - - assert_eq!(post.id, 1); - } - - { - let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); - - assert_eq!(post.id, 5); - } - - { - let post = Mutation::create_post( - db, - post::Model { - id: 0, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::ActiveModel { - id: sea_orm::ActiveValue::Unchanged(6), - title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), - text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) - } - ); - } - - { - let post = Mutation::update_post_by_id( - db, - 1, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }, - ) - .await - .unwrap(); - - assert_eq!( - post, - post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - } - ); - } - - { - let result = Mutation::delete_post(db, 5).await.unwrap(); - - assert_eq!(result.rows_affected, 1); - } - - { - let result = Mutation::delete_all_posts(db).await.unwrap(); - - assert_eq!(result.rows_affected, 5); - } -} diff --git a/examples/tonic_example/core/tests/prepare.rs b/examples/tonic_example/core/tests/prepare.rs deleted file mode 100644 index 556c192c94..0000000000 --- a/examples/tonic_example/core/tests/prepare.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ::entity::post; -use sea_orm::*; - -#[cfg(feature = "mock")] -pub fn prepare_mock_db() -> DatabaseConnection { - MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results([ - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - [post::Model { - id: 6, - title: "Title D".to_owned(), - text: "Text D".to_owned(), - }], - [post::Model { - id: 1, - title: "Title A".to_owned(), - text: "Text A".to_owned(), - }], - [post::Model { - id: 1, - title: "New Title A".to_owned(), - text: "New Text A".to_owned(), - }], - [post::Model { - id: 5, - title: "Title C".to_owned(), - text: "Text C".to_owned(), - }], - ]) - .append_exec_results([ - MockExecResult { - last_insert_id: 6, - rows_affected: 1, - }, - MockExecResult { - last_insert_id: 6, - rows_affected: 5, - }, - ]) - .into_connection() -} diff --git a/examples/tonic_example/entity/Cargo.toml b/examples/tonic_example/entity/Cargo.toml index 7dea367337..9e429a350b 100644 --- a/examples/tonic_example/entity/Cargo.toml +++ b/examples/tonic_example/entity/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" @@ -12,5 +13,5 @@ path = "src/lib.rs" serde = { version = "1", features = ["derive"] } [dependencies.sea-orm] -path = "../../../" # remove this line in your own project -version = "0.12.0" # sea-orm version +path = "../../../" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm version diff --git a/examples/tonic_example/entity/src/lib.rs b/examples/tonic_example/entity/src/lib.rs index e8b6291ac3..b85eeb2ad5 100644 --- a/examples/tonic_example/entity/src/lib.rs +++ b/examples/tonic_example/entity/src/lib.rs @@ -1 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub mod prelude; + pub mod post; diff --git a/examples/tonic_example/entity/src/post.rs b/examples/tonic_example/entity/src/post.rs index 62cde830cf..2e8ff73bbd 100644 --- a/examples/tonic_example/entity/src/post.rs +++ b/examples/tonic_example/entity/src/post.rs @@ -1,8 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] -#[sea_orm(table_name = "posts")] +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] @@ -12,7 +15,4 @@ pub struct Model { pub text: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/tonic_example/entity/src/prelude.rs b/examples/tonic_example/entity/src/prelude.rs new file mode 100644 index 0000000000..0840a758e9 --- /dev/null +++ b/examples/tonic_example/entity/src/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.16 + +pub use super::post::Entity as Post; diff --git a/examples/tonic_example/migration/Cargo.toml b/examples/tonic_example/migration/Cargo.toml index 8f8f42ca78..9b7dfb86c7 100644 --- a/examples/tonic_example/migration/Cargo.toml +++ b/examples/tonic_example/migration/Cargo.toml @@ -1,21 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +entity = { path = "../entity" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [dependencies.sea-orm-migration] -path = "../../../sea-orm-migration" # remove this line in your own project -version = "0.12.0" # sea-orm-migration version features = [ - # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-tokio-rustls", - # "sqlx-postgres", + # Enable following runtime and db backend features if you want to run migration via CLI + "runtime-tokio-native-tls", + "sqlx-sqlite", ] +path = "../../../sea-orm-migration" # remove this line in your own project +version = "~2.0.0-rc.37" # sea-orm-migration version diff --git a/examples/tonic_example/migration/src/lib.rs b/examples/tonic_example/migration/src/lib.rs index af8d9b2ac2..7456138111 100644 --- a/examples/tonic_example/migration/src/lib.rs +++ b/examples/tonic_example/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220120_000001_create_post_table; +mod m20220120_000002_seed_posts; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220120_000001_create_post_table::Migration)] + vec![ + Box::new(m20220120_000001_create_post_table::Migration), + Box::new(m20220120_000002_seed_posts::Migration), + ] } } diff --git a/examples/tonic_example/migration/src/m20220120_000001_create_post_table.rs b/examples/tonic_example/migration/src/m20220120_000001_create_post_table.rs index a2fa0219cd..b92e4af55d 100644 --- a/examples/tonic_example/migration/src/m20220120_000001_create_post_table.rs +++ b/examples/tonic_example/migration/src/m20220120_000001_create_post_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,17 +9,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Posts::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Posts::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Posts::Title).string().not_null()) - .col(ColumnDef::new(Posts::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -27,16 +21,7 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Posts::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Posts { - Table, - Id, - Title, - Text, -} diff --git a/examples/tonic_example/migration/src/m20220120_000002_seed_posts.rs b/examples/tonic_example/migration/src/m20220120_000002_seed_posts.rs new file mode 100644 index 0000000000..320d99826e --- /dev/null +++ b/examples/tonic_example/migration/src/m20220120_000002_seed_posts.rs @@ -0,0 +1,43 @@ +use entity::post; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let seed_data = vec![ + ("First Post", "This is the first post."), + ("Second Post", "This is another post."), + ]; + + for (title, text) in seed_data { + let model = post::ActiveModel { + title: Set(title.to_string()), + text: Set(text.to_string()), + ..Default::default() + }; + model.insert(db).await?; + } + + println!("Posts table seeded successfully."); + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let titles_to_delete = vec!["First Post", "Second Post"]; + post::Entity::delete_many() + .filter(post::Column::Title.is_in(titles_to_delete)) + .exec(db) + .await?; + + println!("Posts seeded data removed."); + Ok(()) + } +} diff --git a/examples/tonic_example/migration/src/main.rs b/examples/tonic_example/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/examples/tonic_example/migration/src/main.rs +++ b/examples/tonic_example/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/examples/tonic_example/src/client.rs b/examples/tonic_example/src/client.rs index f7026e3749..8439004dc2 100644 --- a/examples/tonic_example/src/client.rs +++ b/examples/tonic_example/src/client.rs @@ -1,7 +1,7 @@ -use tonic::transport::Endpoint; use tonic::Request; +use tonic::transport::Endpoint; -use tonic_example_api::post_mod::{blogpost_client::BlogpostClient, PostPerPage}; +use tonic_example_api::post_mod::{PostPerPage, blogpost_client::BlogpostClient}; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/issues/1143/Cargo.toml b/issues/1143/Cargo.toml index a2583faa91..fbd4289952 100644 --- a/issues/1143/Cargo.toml +++ b/issues/1143/Cargo.toml @@ -2,16 +2,17 @@ # A separate workspace [package] -name = "sea-orm-issues-1143" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-1143" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] serde = "1" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } [dependencies.sea-orm] -path = "../../" default-features = false -features = ["macros", "runtime-tokio-native-tls"] +features = ["macros", "runtime-tokio-native-tls"] +path = "../../" diff --git a/issues/1143/src/entity/sea_orm_active_enums.rs b/issues/1143/src/entity/sea_orm_active_enums.rs index 57ba0551ef..cf7a5d4407 100644 --- a/issues/1143/src/entity/sea_orm_active_enums.rs +++ b/issues/1143/src/entity/sea_orm_active_enums.rs @@ -1,7 +1,7 @@ use sea_orm::entity::prelude::*; #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] -#[sea_orm(rs_type = "String", db_type = "String(Some(1))")] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")] pub enum Category { #[sea_orm(string_value = "B")] Big, diff --git a/issues/1278/Cargo.toml b/issues/1278/Cargo.toml index f9be346b40..ea57780e68 100644 --- a/issues/1278/Cargo.toml +++ b/issues/1278/Cargo.toml @@ -2,16 +2,17 @@ # A separate workspace [package] -name = "sea-orm-issues-1278" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-1278" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] serde = "1" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } [dependencies.sea-orm] -path = "../../" default-features = false -features = ["macros", "runtime-tokio-native-tls"] +features = ["macros", "runtime-tokio-native-tls"] +path = "../../" diff --git a/issues/1278/src/entity.rs b/issues/1278/src/entity.rs index 6857d07d96..63603e753c 100644 --- a/issues/1278/src/entity.rs +++ b/issues/1278/src/entity.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "pool")] pub struct Model { #[sea_orm(primary_key)] diff --git a/issues/1357/Cargo.toml b/issues/1357/Cargo.toml index 55e7bfc336..dd7e2f26f8 100644 --- a/issues/1357/Cargo.toml +++ b/issues/1357/Cargo.toml @@ -2,17 +2,18 @@ # A separate workspace [package] -name = "sea-orm-issues-1357" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-1357" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] anyhow = "1" -serde = "1" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } +serde = "1" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } [dependencies.sea-orm] -path = "../../" default-features = false -features = ["macros", "runtime-tokio-native-tls", "sqlx-sqlite"] +features = ["macros", "runtime-tokio-native-tls", "sqlx-sqlite"] +path = "../../" diff --git a/issues/1357/src/entity.rs b/issues/1357/src/entity.rs index 3ad25f9ff7..53c9f328a0 100644 --- a/issues/1357/src/entity.rs +++ b/issues/1357/src/entity.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "pool")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/issues/1473/Cargo.toml b/issues/1473/Cargo.toml new file mode 100644 index 0000000000..6622aa2701 --- /dev/null +++ b/issues/1473/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +# A separate workspace + +[package] +edition = "2024" +name = "sea-orm-issues-1473" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies.sea-orm] +default-features = false +features = ["macros", "runtime-tokio-native-tls"] +path = "../../" diff --git a/issues/1473/src/main.rs b/issues/1473/src/main.rs new file mode 100644 index 0000000000..235b85c5b8 --- /dev/null +++ b/issues/1473/src/main.rs @@ -0,0 +1,16 @@ +use sea_orm::{DeriveIden, Iden}; + +#[derive(DeriveIden)] +enum Character { + Table, + Id, +} + +#[derive(DeriveIden)] +struct Glyph; + +fn main() { + assert_eq!(Character::Table.to_string(), "character"); + assert_eq!(Character::Id.to_string(), "id"); + assert_eq!(Glyph.to_string(), "glyph"); +} diff --git a/issues/1599/entity/Cargo.toml b/issues/1599/entity/Cargo.toml index f34b35bf80..ac180d65d6 100644 --- a/issues/1599/entity/Cargo.toml +++ b/issues/1599/entity/Cargo.toml @@ -1,17 +1,18 @@ [package] -name = "entity" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "entity" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "entity" path = "src/lib.rs" [dependencies] -sea-orm = { path = "../../../" } -seaography = { path = "../../../../seaography", optional = true } -async-graphql = { version = "5", optional = true } +async-graphql = { version = "5", optional = true } +sea-orm = { path = "../../../" } +seaography = { path = "../../../../seaography", optional = true } [features] seaography = ["dep:seaography", "async-graphql", "sea-orm/seaography"] diff --git a/issues/1599/entity/src/cake_filling.rs b/issues/1599/entity/src/cake_filling.rs index 1ce203f687..d8c3357836 100644 --- a/issues/1599/entity/src/cake_filling.rs +++ b/issues/1599/entity/src/cake_filling.rs @@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> &'static str { "cake_filling" } } diff --git a/issues/1599/entity/src/filling.rs b/issues/1599/entity/src/filling.rs index 21a90301b8..afef9eeb4f 100644 --- a/issues/1599/entity/src/filling.rs +++ b/issues/1599/entity/src/filling.rs @@ -55,7 +55,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(None).def(), + Self::Name => ColumnType::String(StringLen::None).def(), Self::VendorId => ColumnType::Integer.def().nullable(), } } diff --git a/issues/1599/graphql/Cargo.toml b/issues/1599/graphql/Cargo.toml index 465d0924f7..c4e0358ed5 100644 --- a/issues/1599/graphql/Cargo.toml +++ b/issues/1599/graphql/Cargo.toml @@ -1,20 +1,26 @@ [package] -name = "graphql" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "graphql" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -poem = { version = "1.3.55" } +async-graphql = { version = "5.0.6", features = [ + "decimal", + "chrono", + "dataloader", + "dynamic-schema", +] } async-graphql-poem = { version = "5.0.6" } -async-graphql = { version = "5.0.6", features = ["decimal", "chrono", "dataloader", "dynamic-schema"] } async-trait = { version = "0.1.64" } dotenv = "0.15.0" +lazy_static = { version = "1.4.0" } +poem = { version = "1.3.55" } tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.16" } -lazy_static = { version = "1.4.0" } -sea-orm = { path = "../../../" } -entity = { path = "../entity", features = ["seaography"] } +entity = { path = "../entity", features = ["seaography"] } +sea-orm = { path = "../../../" } seaography = { path = "../../../../seaography" } diff --git a/issues/1790/Cargo.toml b/issues/1790/Cargo.toml new file mode 100644 index 0000000000..625132123a --- /dev/null +++ b/issues/1790/Cargo.toml @@ -0,0 +1,19 @@ +[workspace] +# A separate workspace + +[package] +edition = "2024" +name = "sea-orm-issues-1790" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies] +anyhow = "1" +serde = "1" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } + +[dependencies.sea-orm] +default-features = false +features = ["macros", "runtime-tokio-native-tls", "sqlx-sqlite"] +path = "../../" diff --git a/issues/1790/insert_test.rs b/issues/1790/insert_test.rs new file mode 100644 index 0000000000..cf30e58c10 --- /dev/null +++ b/issues/1790/insert_test.rs @@ -0,0 +1,58 @@ +mod tests { + // currently ok + #[test] + fn insert_do_nothing_postgres() { + assert_eq!( + Insert::::new() + .add(cake::Model { + id: 1, + name: "Apple Pie".to_owned(), + }) + .on_conflict(OnConflict::new() + .do_nothing() + .to_owned() + ) + .build(DbBackend::Postgres) + .to_string(), + r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie') ON CONFLICT DO NOTHING"#, + ); + } + + //failed to run + #[test] + fn insert_do_nothing_mysql() { + assert_eq!( + Insert::::new() + .add(cake::Model { + id: 1, + name: "Apple Pie".to_owned(), + }) + .on_conflict(OnConflict::new() + .do_nothing() + .to_owned() + ) + .build(DbBackend::Mysql) + .to_string(), + r#"INSERT IGNORE INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"#, + ); + } + + // currently ok + #[test] + fn insert_do_nothing() { + assert_eq!( + Insert::::new() + .add(cake::Model { + id: 1, + name: "Apple Pie".to_owned(), + }) + .on_conflict(OnConflict::new() + .do_nothing() + .to_owned() + ) + .build(DbBackend::Sqlite) + .to_string(), + r#"INSERT IGNORE INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"#, + ); + } +} \ No newline at end of file diff --git a/issues/249/Cargo.toml b/issues/249/Cargo.toml index cadab33f1a..a8f712b90d 100644 --- a/issues/249/Cargo.toml +++ b/issues/249/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["core", "app"] +members = ["service", "app"] diff --git a/issues/249/app/Cargo.toml b/issues/249/app/Cargo.toml index 0834138161..9c7fa2034e 100644 --- a/issues/249/app/Cargo.toml +++ b/issues/249/app/Cargo.toml @@ -1,9 +1,15 @@ [package] -name = "sea-orm-issues-249-app" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-249-app" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -core = { path = "../core" } -sea-orm = { path = "../../../", default-features = false, features = ["macros", "tests-cfg", "sqlx-sqlite", "runtime-async-std-native-tls"] } +sea-orm = { path = "../../../", default-features = false, features = [ + "macros", + "tests-cfg", + "sqlx-sqlite", + "runtime-async-std-native-tls", +] } +service = { path = "../service" } diff --git a/issues/249/app/src/main.rs b/issues/249/app/src/main.rs index 98fbb89db0..296084595c 100644 --- a/issues/249/app/src/main.rs +++ b/issues/249/app/src/main.rs @@ -1,4 +1,4 @@ -use core::clone_a_model; +use service::clone_a_model; use sea_orm::tests_cfg::cake; diff --git a/issues/249/core/Cargo.toml b/issues/249/service/Cargo.toml similarity index 58% rename from issues/249/core/Cargo.toml rename to issues/249/service/Cargo.toml index 0b741c9c66..d7dc835298 100644 --- a/issues/249/core/Cargo.toml +++ b/issues/249/service/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "core" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "service" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] sea-orm = { path = "../../../", default-features = false } diff --git a/issues/249/core/src/lib.rs b/issues/249/service/src/lib.rs similarity index 100% rename from issues/249/core/src/lib.rs rename to issues/249/service/src/lib.rs diff --git a/issues/262/Cargo.toml b/issues/262/Cargo.toml index 6c9ce4e538..3f1e03e2f5 100644 --- a/issues/262/Cargo.toml +++ b/issues/262/Cargo.toml @@ -2,11 +2,16 @@ # A separate workspace [package] -name = "sea-orm-issues-262" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-262" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-async-std-native-tls", "debug-print" ] } async-std = { version = "1", features = ["attributes", "tokio1"] } +sea-orm = { path = "../../", features = [ + "sqlx-all", + "runtime-async-std-native-tls", + "debug-print", +] } diff --git a/issues/319/Cargo.toml b/issues/319/Cargo.toml index 2269214d86..060830f71f 100644 --- a/issues/319/Cargo.toml +++ b/issues/319/Cargo.toml @@ -2,14 +2,14 @@ # A separate workspace [package] -name = "sea-orm-issues-319" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-319" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] async-std = { version = "1", features = ["attributes", "tokio1"] } -serde = { version = "1", features = ["derive"] } sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls", @@ -17,3 +17,4 @@ sea-orm = { path = "../../", features = [ "with-chrono", "macros", ], default-features = false } +serde = { version = "1", features = ["derive"] } diff --git a/issues/324/Cargo.toml b/issues/324/Cargo.toml index 9effe6d2f6..16ca96aab0 100644 --- a/issues/324/Cargo.toml +++ b/issues/324/Cargo.toml @@ -2,10 +2,14 @@ # A separate workspace [package] -name = "sea-orm-issues-324" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-324" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls" ]} +sea-orm = { path = "../../", features = [ + "sqlx-mysql", + "runtime-async-std-native-tls", +] } diff --git a/issues/352/Cargo.toml b/issues/352/Cargo.toml index 015b19b7e5..8ea5698d6a 100644 --- a/issues/352/Cargo.toml +++ b/issues/352/Cargo.toml @@ -2,10 +2,14 @@ # A separate workspace [package] -name = "sea-orm-issues-352" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-352" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls" ]} +sea-orm = { path = "../../", features = [ + "sqlx-mysql", + "runtime-async-std-native-tls", +] } diff --git a/issues/356/Cargo.toml b/issues/356/Cargo.toml index ed64bae46c..1cf039cd0d 100644 --- a/issues/356/Cargo.toml +++ b/issues/356/Cargo.toml @@ -2,10 +2,14 @@ # A separate workspace [package] -name = "sea-orm-issues-356" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-356" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls" ]} +sea-orm = { path = "../../", features = [ + "sqlx-mysql", + "runtime-async-std-native-tls", +] } diff --git a/issues/400/Cargo.toml b/issues/400/Cargo.toml index d6200d12be..c406b43344 100644 --- a/issues/400/Cargo.toml +++ b/issues/400/Cargo.toml @@ -2,10 +2,14 @@ # A separate workspace [package] -name = "sea-orm-issues-400" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-400" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls" ]} +sea-orm = { path = "../../", features = [ + "sqlx-mysql", + "runtime-async-std-native-tls", +] } diff --git a/issues/471/Cargo.toml b/issues/471/Cargo.toml index 26368c7d06..0119734e3f 100644 --- a/issues/471/Cargo.toml +++ b/issues/471/Cargo.toml @@ -2,21 +2,28 @@ # A separate workspace [package] -name = "sea-orm-issues-400-471" -version = "0.1.0" -authors = ["Sebastian Pütz "] -edition = "2021" -publish = false +authors = ["Sebastian Pütz "] +edition = "2024" +name = "sea-orm-issues-400-471" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -tokio = { version = "1.14", features = ["full"] } -anyhow = "1" -dotenvy = "0.15" -futures-util = "0.3" -serde = "1" +anyhow = "1" +dotenvy = "0.15" +futures-util = "0.3" +serde = "1" +tokio = { version = "1.14", features = ["full"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } [dependencies.sea-orm] -path = "../../" # remove this line in your own project -features = ["macros", "mock", "sqlx-all", "runtime-tokio-rustls", "debug-print"] default-features = false +features = [ + "macros", + "mock", + "sqlx-all", + "runtime-tokio-rustls", + "debug-print", +] +path = "../../" # remove this line in your own project diff --git a/issues/471/src/main.rs b/issues/471/src/main.rs index d4e93a950d..0072927caa 100644 --- a/issues/471/src/main.rs +++ b/issues/471/src/main.rs @@ -8,7 +8,9 @@ use std::env; #[tokio::main] async fn main() -> anyhow::Result<()> { - env::set_var("RUST_LOG", "debug"); + unsafe { + env::set_var("RUST_LOG", "debug"); + } tracing_subscriber::fmt::init(); dotenvy::dotenv().ok(); diff --git a/issues/630/Cargo.toml b/issues/630/Cargo.toml index 5d9dc1da19..6825437fc0 100644 --- a/issues/630/Cargo.toml +++ b/issues/630/Cargo.toml @@ -2,21 +2,28 @@ # A separate workspace [package] -name = "sea-orm-issues-630" -version = "0.1.0" -authors = ["Erik Rhodes "] -edition = "2021" -publish = false +authors = ["Erik Rhodes "] +edition = "2024" +name = "sea-orm-issues-630" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] serde = "1" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } [dependencies.sea-orm] -path = "../../" default-features = false -features = ["macros", "runtime-tokio-native-tls", "debug-print", "with-json", "with-chrono"] +features = [ + "macros", + "runtime-tokio-native-tls", + "debug-print", + "with-json", + "with-chrono", +] +path = "../../" [features] -default = ["sqlx-mysql"] +default = ["sqlx-mysql"] sqlx-mysql = ["sea-orm/sqlx-mysql"] diff --git a/issues/693/Cargo.toml b/issues/693/Cargo.toml index cf9f4ed488..c4aa08ba61 100644 --- a/issues/693/Cargo.toml +++ b/issues/693/Cargo.toml @@ -2,21 +2,22 @@ # A separate workspace [package] -name = "sea-orm-issues-693" -version = "0.1.0" -authors = ["bleuse "] -edition = "2021" -publish = false +authors = ["bleuse "] +edition = "2024" +name = "sea-orm-issues-693" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -tokio = { version = "1.14", features = ["full"] } -anyhow = "1" -dotenvy = "0.15" -futures-util = "0.3" -serde = "1" +anyhow = "1" +dotenvy = "0.15" +futures-util = "0.3" +serde = "1" +tokio = { version = "1.14", features = ["full"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } [dependencies.sea-orm] -path = "../../" # remove this line in your own project -features = ["runtime-tokio-rustls", "sqlx-mysql", "macros"] default-features = false +features = ["runtime-tokio-rustls", "sqlx-mysql", "macros"] +path = "../../" # remove this line in your own project diff --git a/issues/86/Cargo.toml b/issues/86/Cargo.toml index 6aee3bed41..26e12da855 100644 --- a/issues/86/Cargo.toml +++ b/issues/86/Cargo.toml @@ -2,13 +2,18 @@ # A separate workspace [package] -name = "sea-orm-issues-86" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "sea-orm-issues-86" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] -sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-tokio-native-tls", "debug-print" ] } +sea-orm = { path = "../../", features = [ + "sqlx-all", + "runtime-tokio-native-tls", + "debug-print", +] } tokio = { version = "1", features = ["full"] } -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/issues/892/Cargo.toml b/issues/892/Cargo.toml index 935e9e998e..47cdde573a 100644 --- a/issues/892/Cargo.toml +++ b/issues/892/Cargo.toml @@ -2,16 +2,17 @@ # A separate workspace [package] -name = "sea-orm-issues-892" -version = "0.1.0" -authors = [] -edition = "2021" -publish = false +authors = [] +edition = "2024" +name = "sea-orm-issues-892" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [dependencies] tokio = { version = "1.20.0", features = ["rt-multi-thread", "macros"] } [dependencies.sea-orm] -path = "../../" # remove this line in your own project -features = ["runtime-tokio-rustls", "tests-cfg", "sqlx-sqlite", "macros"] default-features = false +features = ["runtime-tokio-rustls", "tests-cfg", "sqlx-sqlite", "macros"] +path = "../../" # remove this line in your own project diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 521706a0a1..0000000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -format_code_in_doc_comments=true \ No newline at end of file diff --git a/sea-orm-arrow/Cargo.toml b/sea-orm-arrow/Cargo.toml new file mode 100644 index 0000000000..288d49c4a6 --- /dev/null +++ b/sea-orm-arrow/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = ["Chris Tsang "] +categories = ["database"] +description = "Arrow integration for SeaORM" +documentation = "https://docs.rs/sea-orm-arrow" +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" +keywords = ["orm", "arrow", "parquet", "sea-orm"] +license = "MIT OR Apache-2.0" +name = "sea-orm-arrow" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.4" + +[dependencies] +arrow = { version = "57", default-features = false } +sea-query = { version = "1.0.0-rc", default-features = false, features = [ + "thread-safe", +] } +thiserror = { version = "2", default-features = false } + +[features] +prettyprint = ["arrow/prettyprint"] +with-bigdecimal = ["sea-query/with-bigdecimal"] +with-chrono = ["sea-query/with-chrono"] +with-rust_decimal = ["sea-query/with-rust_decimal"] +with-time = ["sea-query/with-time"] diff --git a/sea-orm-arrow/src/lib.rs b/sea-orm-arrow/src/lib.rs new file mode 100644 index 0000000000..7b0dc77491 --- /dev/null +++ b/sea-orm-arrow/src/lib.rs @@ -0,0 +1,1550 @@ +pub use arrow; + +use arrow::array::*; +use arrow::datatypes::i256; +use sea_query::{ColumnType, Value}; + +// --------------------------------------------------------------------------- +// Error type +// --------------------------------------------------------------------------- + +/// Errors that can occur when converting between SeaORM [`Value`]s and Arrow arrays. +#[derive(Debug, thiserror::Error)] +pub enum ArrowError { + /// The Arrow array type is incompatible with the target SeaORM column type. + #[error("expected {expected} for column type {col_type}, got Arrow type {actual}")] + TypeMismatch { + expected: &'static str, + col_type: &'static str, + actual: String, + }, + + /// A value lies outside the representable range for the target type. + #[error("{0}")] + OutOfRange(String), + + /// The column type or Arrow data type is not supported for conversion. + #[error("{0}")] + Unsupported(String), +} + +fn type_err(expected: &'static str, col_type: &'static str, array: &dyn Array) -> ArrowError { + ArrowError::TypeMismatch { + expected, + col_type, + actual: format!("{:?}", array.data_type()), + } +} + +// --------------------------------------------------------------------------- +// Arrow -> Value +// --------------------------------------------------------------------------- + +/// Extract a [`Value`] from an Arrow array at the given row index, +/// based on the expected [`ColumnType`] from the entity definition. +/// +/// For date/time column types, this produces chrono `Value` variants when +/// the `with-chrono` feature is enabled, or time-crate variants when only +/// `with-time` is enabled. +pub fn arrow_array_to_value( + array: &dyn Array, + col_type: &ColumnType, + row: usize, +) -> Result { + if array.is_null(row) { + return Ok(null_value_for_type(col_type)); + } + match col_type { + ColumnType::TinyInteger => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("Int8Array", "TinyInteger", array))?; + Ok(Value::TinyInt(Some(arr.value(row)))) + } + ColumnType::SmallInteger => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("Int16Array", "SmallInteger", array))?; + Ok(Value::SmallInt(Some(arr.value(row)))) + } + ColumnType::Integer => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("Int32Array", "Integer", array))?; + Ok(Value::Int(Some(arr.value(row)))) + } + ColumnType::BigInteger => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("Int64Array", "BigInteger", array))?; + Ok(Value::BigInt(Some(arr.value(row)))) + } + ColumnType::TinyUnsigned => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("UInt8Array", "TinyUnsigned", array))?; + Ok(Value::TinyUnsigned(Some(arr.value(row)))) + } + ColumnType::SmallUnsigned => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("UInt16Array", "SmallUnsigned", array))?; + Ok(Value::SmallUnsigned(Some(arr.value(row)))) + } + ColumnType::Unsigned => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("UInt32Array", "Unsigned", array))?; + Ok(Value::Unsigned(Some(arr.value(row)))) + } + ColumnType::BigUnsigned => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("UInt64Array", "BigUnsigned", array))?; + Ok(Value::BigUnsigned(Some(arr.value(row)))) + } + ColumnType::Float => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("Float32Array", "Float", array))?; + Ok(Value::Float(Some(arr.value(row)))) + } + ColumnType::Double => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("Float64Array", "Double", array))?; + Ok(Value::Double(Some(arr.value(row)))) + } + ColumnType::String(_) | ColumnType::Text | ColumnType::Char(_) => { + if let Some(arr) = array.as_any().downcast_ref::() { + Ok(Value::String(Some(arr.value(row).to_owned()))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + Ok(Value::String(Some(arr.value(row).to_owned()))) + } else { + Err(type_err( + "StringArray or LargeStringArray", + "String/Text", + array, + )) + } + } + ColumnType::Boolean => { + let arr = array + .as_any() + .downcast_ref::() + .ok_or_else(|| type_err("BooleanArray", "Boolean", array))?; + Ok(Value::Bool(Some(arr.value(row)))) + } + // Binary types + ColumnType::Binary(_) | ColumnType::VarBinary(_) => arrow_to_bytes(array, row), + // Decimal types + ColumnType::Decimal(_) | ColumnType::Money(_) => arrow_to_decimal(array, row), + // Date/time types: delegate to feature-gated helpers. + // Prefer chrono when available; fall back to time crate. + #[cfg(feature = "with-chrono")] + ColumnType::Date => arrow_to_chrono_date(array, row), + #[cfg(feature = "with-chrono")] + ColumnType::Time => arrow_to_chrono_time(array, row), + #[cfg(feature = "with-chrono")] + ColumnType::DateTime | ColumnType::Timestamp => arrow_to_chrono_datetime(array, row), + #[cfg(feature = "with-chrono")] + ColumnType::TimestampWithTimeZone => arrow_to_chrono_datetime_utc(array, row), + + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::Date => arrow_to_time_date(array, row), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::Time => arrow_to_time_time(array, row), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::DateTime | ColumnType::Timestamp => arrow_to_time_datetime(array, row), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::TimestampWithTimeZone => arrow_to_time_datetime_tz(array, row), + + _ => Err(ArrowError::Unsupported(format!( + "Unsupported column type for Arrow conversion: {col_type:?}" + ))), + } +} + +/// When both `with-chrono` and `with-time` are enabled, this provides the +/// time-crate alternative for date/time columns. Called as a fallback when +/// the chrono Value variant doesn't match the model's field type. +#[cfg(all(feature = "with-chrono", feature = "with-time"))] +pub fn arrow_array_to_value_alt( + array: &dyn Array, + col_type: &ColumnType, + row: usize, +) -> Result, ArrowError> { + if array.is_null(row) { + return Ok(Some(null_value_for_type_time(col_type))); + } + match col_type { + ColumnType::Date => arrow_to_time_date(array, row).map(Some), + ColumnType::Time => arrow_to_time_time(array, row).map(Some), + ColumnType::DateTime | ColumnType::Timestamp => { + arrow_to_time_datetime(array, row).map(Some) + } + ColumnType::TimestampWithTimeZone => arrow_to_time_datetime_tz(array, row).map(Some), + _ => Ok(None), + } +} + +/// Returns true for ColumnTypes that may need a chrono->time fallback. +pub fn is_datetime_column(col_type: &ColumnType) -> bool { + matches!( + col_type, + ColumnType::Date + | ColumnType::Time + | ColumnType::DateTime + | ColumnType::Timestamp + | ColumnType::TimestampWithTimeZone + ) +} + +// --------------------------------------------------------------------------- +// Binary helpers +// --------------------------------------------------------------------------- + +fn arrow_to_bytes(array: &dyn Array, row: usize) -> Result { + if let Some(arr) = array.as_any().downcast_ref::() { + return Ok(Value::Bytes(Some(arr.value(row).to_vec()))); + } + if let Some(arr) = array.as_any().downcast_ref::() { + return Ok(Value::Bytes(Some(arr.value(row).to_vec()))); + } + if let Some(arr) = array.as_any().downcast_ref::() { + return Ok(Value::Bytes(Some(arr.value(row).to_vec()))); + } + Err(type_err( + "BinaryArray, LargeBinaryArray, or FixedSizeBinaryArray", + "Binary/VarBinary", + array, + )) +} + +// --------------------------------------------------------------------------- +// Decimal helpers +// --------------------------------------------------------------------------- + +/// Convert Arrow Decimal128Array or Decimal256Array to a decimal Value. +/// Prefers rust_decimal when available and precision fits, otherwise bigdecimal. +fn arrow_to_decimal(array: &dyn Array, row: usize) -> Result { + if let Some(arr) = array.as_any().downcast_ref::() { + let value = arr.value(row); + let precision = arr.precision(); + let scale = arr.scale(); + return decimal128_to_value(value, precision, scale); + } + + if let Some(arr) = array.as_any().downcast_ref::() { + let value = arr.value(row); + let precision = arr.precision(); + let scale = arr.scale(); + return decimal64_to_value(value, precision, scale); + } + + if let Some(arr) = array.as_any().downcast_ref::() { + let value = arr.value(row); + let precision = arr.precision(); + let scale = arr.scale(); + return decimal256_to_value(value, precision, scale); + } + + Err(type_err( + "Decimal64Array, Decimal128Array, or Decimal256Array", + "Decimal", + array, + )) +} + +#[cfg(feature = "with-rust_decimal")] +fn decimal64_to_value(value: i64, _precision: u8, scale: i8) -> Result { + use sea_query::prelude::Decimal; + + if scale < 0 { + #[cfg(feature = "with-bigdecimal")] + return decimal64_to_bigdecimal(value, scale); + + #[cfg(not(feature = "with-bigdecimal"))] + return Err(ArrowError::Unsupported(format!( + "Decimal64 with negative scale={scale} not supported by rust_decimal. \ + Enable 'with-bigdecimal' feature." + ))); + } + + let decimal = Decimal::from_i128_with_scale(value as i128, scale as u32); + Ok(Value::Decimal(Some(decimal))) +} + +#[cfg(not(feature = "with-rust_decimal"))] +fn decimal64_to_value(_value: i64, _precision: u8, _scale: i8) -> Result { + #[cfg(feature = "with-bigdecimal")] + return decimal64_to_bigdecimal(_value, _scale); + + #[cfg(not(feature = "with-bigdecimal"))] + Err(ArrowError::Unsupported( + "Decimal64Array requires 'with-rust_decimal' or 'with-bigdecimal' feature".into(), + )) +} + +#[cfg(feature = "with-bigdecimal")] +fn decimal64_to_bigdecimal(value: i64, scale: i8) -> Result { + use sea_query::prelude::bigdecimal::{BigDecimal, num_bigint::BigInt}; + + let bigint = BigInt::from(value); + let decimal = BigDecimal::new(bigint, scale as i64); + Ok(Value::BigDecimal(Some(Box::new(decimal)))) +} + +#[cfg(feature = "with-rust_decimal")] +fn decimal128_to_value(value: i128, precision: u8, scale: i8) -> Result { + use sea_query::prelude::Decimal; + + if precision > 28 || scale > 28 || scale < 0 { + #[cfg(feature = "with-bigdecimal")] + return decimal128_to_bigdecimal(value, scale); + + #[cfg(not(feature = "with-bigdecimal"))] + return Err(ArrowError::Unsupported(format!( + "Decimal128 with precision={precision}, scale={scale} exceeds rust_decimal limits \ + (max precision=28, scale=0-28). Enable 'with-bigdecimal' feature for arbitrary precision." + ))); + } + + let decimal = Decimal::from_i128_with_scale(value, scale as u32); + Ok(Value::Decimal(Some(decimal))) +} + +#[cfg(not(feature = "with-rust_decimal"))] +fn decimal128_to_value(_value: i128, _precision: u8, _scale: i8) -> Result { + #[cfg(feature = "with-bigdecimal")] + return decimal128_to_bigdecimal(_value, _scale); + + #[cfg(not(feature = "with-bigdecimal"))] + Err(ArrowError::Unsupported( + "Decimal128Array requires 'with-rust_decimal' or 'with-bigdecimal' feature".into(), + )) +} + +#[cfg(feature = "with-bigdecimal")] +fn decimal128_to_bigdecimal(value: i128, scale: i8) -> Result { + use sea_query::prelude::bigdecimal::{BigDecimal, num_bigint::BigInt}; + + let bigint = BigInt::from(value); + let decimal = BigDecimal::new(bigint, scale as i64); + Ok(Value::BigDecimal(Some(Box::new(decimal)))) +} + +fn decimal256_to_value(_value: i256, _precision: u8, _scale: i8) -> Result { + #[cfg(feature = "with-bigdecimal")] + { + use sea_query::prelude::bigdecimal::{ + BigDecimal, + num_bigint::{BigInt, Sign}, + }; + + let bytes = _value.to_be_bytes(); + + let (sign, magnitude) = if _value.is_negative() { + let mut abs_bytes = [0u8; 32]; + let mut carry = true; + + for i in (0..32).rev() { + abs_bytes[i] = !bytes[i]; + if carry { + if abs_bytes[i] == 255 { + abs_bytes[i] = 0; + } else { + abs_bytes[i] += 1; + carry = false; + } + } + } + + (Sign::Minus, abs_bytes.to_vec()) + } else if _value == i256::ZERO { + (Sign::NoSign, vec![0]) + } else { + let first_nonzero = bytes.iter().position(|&b| b != 0).unwrap_or(31); + (Sign::Plus, bytes[first_nonzero..].to_vec()) + }; + + let bigint = BigInt::from_bytes_be(sign, &magnitude); + let decimal = BigDecimal::new(bigint, _scale as i64); + return Ok(Value::BigDecimal(Some(Box::new(decimal)))); + } + + #[cfg(not(feature = "with-bigdecimal"))] + Err(ArrowError::Unsupported( + "Decimal256Array requires 'with-bigdecimal' feature for arbitrary precision support".into(), + )) +} + +// --------------------------------------------------------------------------- +// Chrono date/time helpers +// --------------------------------------------------------------------------- + +#[cfg(feature = "with-chrono")] +fn arrow_to_chrono_date(array: &dyn Array, row: usize) -> Result { + use sea_query::prelude::chrono::NaiveDate; + let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid date"); + + if let Some(arr) = array.as_any().downcast_ref::() { + let days = arr.value(row); + let date = epoch + .checked_add_signed(sea_query::prelude::chrono::Duration::days(days as i64)) + .ok_or_else(|| ArrowError::OutOfRange(format!("Date32 value {days} out of range")))?; + Ok(Value::ChronoDate(Some(date))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ms = arr.value(row); + let date = epoch + .checked_add_signed(sea_query::prelude::chrono::Duration::milliseconds(ms)) + .ok_or_else(|| ArrowError::OutOfRange(format!("Date64 value {ms} out of range")))?; + Ok(Value::ChronoDate(Some(date))) + } else { + Err(type_err("Date32Array or Date64Array", "Date", array)) + } +} + +#[cfg(feature = "with-chrono")] +fn arrow_to_chrono_time(array: &dyn Array, row: usize) -> Result { + use sea_query::prelude::chrono::NaiveTime; + + if let Some(arr) = array.as_any().downcast_ref::() { + let secs = arr.value(row) as u32; + let t = NaiveTime::from_num_seconds_from_midnight_opt(secs, 0).ok_or_else(|| { + ArrowError::OutOfRange(format!("Time32Second value {secs} out of range")) + })?; + Ok(Value::ChronoTime(Some(t))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ms = arr.value(row); + let secs = (ms / 1_000) as u32; + let nanos = ((ms % 1_000) * 1_000_000) as u32; + let t = NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).ok_or_else(|| { + ArrowError::OutOfRange(format!("Time32Millisecond value {ms} out of range")) + })?; + Ok(Value::ChronoTime(Some(t))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let us = arr.value(row); + let secs = (us / 1_000_000) as u32; + let nanos = ((us % 1_000_000) * 1_000) as u32; + let t = NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).ok_or_else(|| { + ArrowError::OutOfRange(format!("Time64Microsecond value {us} out of range")) + })?; + Ok(Value::ChronoTime(Some(t))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ns = arr.value(row); + let secs = (ns / 1_000_000_000) as u32; + let nanos = (ns % 1_000_000_000) as u32; + let t = NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).ok_or_else(|| { + ArrowError::OutOfRange(format!("Time64Nanosecond value {ns} out of range")) + })?; + Ok(Value::ChronoTime(Some(t))) + } else { + Err(type_err("Time32/Time64 Array", "Time", array)) + } +} + +#[cfg(feature = "with-chrono")] +fn arrow_timestamp_to_utc( + array: &dyn Array, + row: usize, +) -> Result, ArrowError> { + use sea_query::prelude::chrono::{DateTime, Utc}; + + if let Some(arr) = array.as_any().downcast_ref::() { + DateTime::::from_timestamp(arr.value(row), 0) + .ok_or_else(|| ArrowError::OutOfRange("Timestamp seconds out of range".into())) + } else if let Some(arr) = array.as_any().downcast_ref::() { + DateTime::::from_timestamp_millis(arr.value(row)) + .ok_or_else(|| ArrowError::OutOfRange("Timestamp milliseconds out of range".into())) + } else if let Some(arr) = array.as_any().downcast_ref::() { + DateTime::::from_timestamp_micros(arr.value(row)) + .ok_or_else(|| ArrowError::OutOfRange("Timestamp microseconds out of range".into())) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let nanos = arr.value(row); + let secs = nanos.div_euclid(1_000_000_000); + let nsec = nanos.rem_euclid(1_000_000_000) as u32; + DateTime::::from_timestamp(secs, nsec) + .ok_or_else(|| ArrowError::OutOfRange("Timestamp nanoseconds out of range".into())) + } else { + Err(type_err( + "TimestampSecond/Millisecond/Microsecond/NanosecondArray", + "DateTime/Timestamp", + array, + )) + } +} + +#[cfg(feature = "with-chrono")] +fn arrow_to_chrono_datetime(array: &dyn Array, row: usize) -> Result { + let dt = arrow_timestamp_to_utc(array, row)?; + Ok(Value::ChronoDateTime(Some(dt.naive_utc()))) +} + +#[cfg(feature = "with-chrono")] +fn arrow_to_chrono_datetime_utc(array: &dyn Array, row: usize) -> Result { + let dt = arrow_timestamp_to_utc(array, row)?; + Ok(Value::ChronoDateTimeUtc(Some(dt))) +} + +// --------------------------------------------------------------------------- +// Time-crate date/time helpers +// --------------------------------------------------------------------------- + +#[cfg(feature = "with-time")] +fn arrow_to_time_date(array: &dyn Array, row: usize) -> Result { + const EPOCH_JULIAN: i32 = 2_440_588; + + if let Some(arr) = array.as_any().downcast_ref::() { + let days = arr.value(row); + let date = + sea_query::prelude::time::Date::from_julian_day(EPOCH_JULIAN + days).map_err(|e| { + ArrowError::OutOfRange(format!("Date32 value {days} out of range: {e}")) + })?; + Ok(Value::TimeDate(Some(date))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ms = arr.value(row); + let days = (ms / 86_400_000) as i32; + let date = sea_query::prelude::time::Date::from_julian_day(EPOCH_JULIAN + days) + .map_err(|e| ArrowError::OutOfRange(format!("Date64 value {ms} out of range: {e}")))?; + Ok(Value::TimeDate(Some(date))) + } else { + Err(type_err("Date32Array or Date64Array", "Date", array)) + } +} + +#[cfg(feature = "with-time")] +fn arrow_to_time_time(array: &dyn Array, row: usize) -> Result { + if let Some(arr) = array.as_any().downcast_ref::() { + let secs = arr.value(row); + let t = sea_query::prelude::time::Time::from_hms( + (secs / 3600) as u8, + ((secs % 3600) / 60) as u8, + (secs % 60) as u8, + ) + .map_err(|e| { + ArrowError::OutOfRange(format!("Time32Second value {secs} out of range: {e}")) + })?; + Ok(Value::TimeTime(Some(t))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ms = arr.value(row); + let total_secs = ms / 1_000; + let nanos = ((ms % 1_000) * 1_000_000) as u32; + let t = sea_query::prelude::time::Time::from_hms_nano( + (total_secs / 3600) as u8, + ((total_secs % 3600) / 60) as u8, + (total_secs % 60) as u8, + nanos, + ) + .map_err(|e| { + ArrowError::OutOfRange(format!("Time32Millisecond value {ms} out of range: {e}")) + })?; + Ok(Value::TimeTime(Some(t))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let us = arr.value(row); + let total_secs = us / 1_000_000; + let nanos = ((us % 1_000_000) * 1_000) as u32; + let t = sea_query::prelude::time::Time::from_hms_nano( + (total_secs / 3600) as u8, + ((total_secs % 3600) / 60) as u8, + (total_secs % 60) as u8, + nanos, + ) + .map_err(|e| { + ArrowError::OutOfRange(format!("Time64Microsecond value {us} out of range: {e}")) + })?; + Ok(Value::TimeTime(Some(t))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ns = arr.value(row); + let total_secs = ns / 1_000_000_000; + let nanos = (ns % 1_000_000_000) as u32; + let t = sea_query::prelude::time::Time::from_hms_nano( + (total_secs / 3600) as u8, + ((total_secs % 3600) / 60) as u8, + (total_secs % 60) as u8, + nanos, + ) + .map_err(|e| { + ArrowError::OutOfRange(format!("Time64Nanosecond value {ns} out of range: {e}")) + })?; + Ok(Value::TimeTime(Some(t))) + } else { + Err(type_err("Time32/Time64 Array", "Time", array)) + } +} + +#[cfg(feature = "with-time")] +fn arrow_timestamp_to_offset_dt( + array: &dyn Array, + row: usize, +) -> Result { + if let Some(arr) = array.as_any().downcast_ref::() { + sea_query::prelude::time::OffsetDateTime::from_unix_timestamp(arr.value(row)) + .map_err(|e| ArrowError::OutOfRange(format!("Timestamp seconds out of range: {e}"))) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let ms = arr.value(row); + sea_query::prelude::time::OffsetDateTime::from_unix_timestamp_nanos(ms as i128 * 1_000_000) + .map_err(|e| { + ArrowError::OutOfRange(format!("Timestamp milliseconds out of range: {e}")) + }) + } else if let Some(arr) = array.as_any().downcast_ref::() { + let us = arr.value(row); + sea_query::prelude::time::OffsetDateTime::from_unix_timestamp_nanos(us as i128 * 1_000) + .map_err(|e| { + ArrowError::OutOfRange(format!("Timestamp microseconds out of range: {e}")) + }) + } else if let Some(arr) = array.as_any().downcast_ref::() { + sea_query::prelude::time::OffsetDateTime::from_unix_timestamp_nanos(arr.value(row) as i128) + .map_err(|e| ArrowError::OutOfRange(format!("Timestamp nanoseconds out of range: {e}"))) + } else { + Err(type_err( + "TimestampSecond/Millisecond/Microsecond/NanosecondArray", + "DateTime/Timestamp", + array, + )) + } +} + +#[cfg(feature = "with-time")] +fn arrow_to_time_datetime(array: &dyn Array, row: usize) -> Result { + let odt = arrow_timestamp_to_offset_dt(array, row)?; + Ok(Value::TimeDateTime(Some( + sea_query::prelude::time::PrimitiveDateTime::new(odt.date(), odt.time()), + ))) +} + +#[cfg(feature = "with-time")] +fn arrow_to_time_datetime_tz(array: &dyn Array, row: usize) -> Result { + let odt = arrow_timestamp_to_offset_dt(array, row)?; + Ok(Value::TimeDateTimeWithTimeZone(Some(odt))) +} + +// --------------------------------------------------------------------------- +// Null value helpers +// --------------------------------------------------------------------------- + +fn null_value_for_type(col_type: &ColumnType) -> Value { + match col_type { + ColumnType::TinyInteger => Value::TinyInt(None), + ColumnType::SmallInteger => Value::SmallInt(None), + ColumnType::Integer => Value::Int(None), + ColumnType::BigInteger => Value::BigInt(None), + ColumnType::TinyUnsigned => Value::TinyUnsigned(None), + ColumnType::SmallUnsigned => Value::SmallUnsigned(None), + ColumnType::Unsigned => Value::Unsigned(None), + ColumnType::BigUnsigned => Value::BigUnsigned(None), + ColumnType::Float => Value::Float(None), + ColumnType::Double => Value::Double(None), + ColumnType::String(_) | ColumnType::Text | ColumnType::Char(_) => Value::String(None), + ColumnType::Binary(_) | ColumnType::VarBinary(_) => Value::Bytes(None), + ColumnType::Boolean => Value::Bool(None), + #[cfg(feature = "with-rust_decimal")] + ColumnType::Decimal(_) | ColumnType::Money(_) => Value::Decimal(None), + #[cfg(all(feature = "with-bigdecimal", not(feature = "with-rust_decimal")))] + ColumnType::Decimal(_) | ColumnType::Money(_) => Value::BigDecimal(None), + #[cfg(feature = "with-chrono")] + ColumnType::Date => Value::ChronoDate(None), + #[cfg(feature = "with-chrono")] + ColumnType::Time => Value::ChronoTime(None), + #[cfg(feature = "with-chrono")] + ColumnType::DateTime | ColumnType::Timestamp => Value::ChronoDateTime(None), + #[cfg(feature = "with-chrono")] + ColumnType::TimestampWithTimeZone => Value::ChronoDateTimeUtc(None), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::Date => Value::TimeDate(None), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::Time => Value::TimeTime(None), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::DateTime | ColumnType::Timestamp => Value::TimeDateTime(None), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + ColumnType::TimestampWithTimeZone => Value::TimeDateTimeWithTimeZone(None), + _ => Value::Int(None), + } +} + +/// Null values for the time crate variants, used by the alt-value fallback path. +#[cfg(all(feature = "with-chrono", feature = "with-time"))] +fn null_value_for_type_time(col_type: &ColumnType) -> Value { + match col_type { + ColumnType::Date => Value::TimeDate(None), + ColumnType::Time => Value::TimeTime(None), + ColumnType::DateTime | ColumnType::Timestamp => Value::TimeDateTime(None), + ColumnType::TimestampWithTimeZone => Value::TimeDateTimeWithTimeZone(None), + _ => null_value_for_type(col_type), + } +} + +// --------------------------------------------------------------------------- +// Value -> Arrow +// --------------------------------------------------------------------------- + +/// Convert a slice of [`Value`]s to an Arrow array matching the +/// target [`DataType`](arrow::datatypes::DataType). +/// +/// `Value::Variant(None)` (SQL NULL) entries become null in the array. +pub fn values_to_arrow_array( + values: &[Value], + data_type: &arrow::datatypes::DataType, +) -> Result, ArrowError> { + use arrow::datatypes::{DataType, TimeUnit}; + use std::sync::Arc; + + match data_type { + DataType::Int8 => { + let arr: Int8Array = values + .iter() + .map(|v| match v { + Value::TinyInt(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Int16 => { + let arr: Int16Array = values + .iter() + .map(|v| match v { + Value::SmallInt(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Int32 => { + let arr: Int32Array = values + .iter() + .map(|v| match v { + Value::Int(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Int64 => { + let arr: Int64Array = values + .iter() + .map(|v| match v { + Value::BigInt(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt8 => { + let arr: UInt8Array = values + .iter() + .map(|v| match v { + Value::TinyUnsigned(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt16 => { + let arr: UInt16Array = values + .iter() + .map(|v| match v { + Value::SmallUnsigned(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt32 => { + let arr: UInt32Array = values + .iter() + .map(|v| match v { + Value::Unsigned(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt64 => { + let arr: UInt64Array = values + .iter() + .map(|v| match v { + Value::BigUnsigned(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Float32 => { + let arr: Float32Array = values + .iter() + .map(|v| match v { + Value::Float(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Float64 => { + let arr: Float64Array = values + .iter() + .map(|v| match v { + Value::Double(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Boolean => { + let arr: BooleanArray = values + .iter() + .map(|v| match v { + Value::Bool(inner) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Utf8 => { + let strs: Vec> = values + .iter() + .map(|v| match v { + Value::String(Some(s)) => Some(s.as_str()), + _ => None, + }) + .collect(); + Ok(Arc::new(StringArray::from(strs))) + } + DataType::LargeUtf8 => { + let strs: Vec> = values + .iter() + .map(|v| match v { + Value::String(Some(s)) => Some(s.as_str()), + _ => None, + }) + .collect(); + Ok(Arc::new(LargeStringArray::from(strs))) + } + DataType::Binary => { + let bufs: Vec> = values + .iter() + .map(|v| match v { + Value::Bytes(Some(b)) => Some(b.as_slice()), + _ => None, + }) + .collect(); + Ok(Arc::new(BinaryArray::from(bufs))) + } + DataType::LargeBinary => { + let bufs: Vec> = values + .iter() + .map(|v| match v { + Value::Bytes(Some(b)) => Some(b.as_slice()), + _ => None, + }) + .collect(); + Ok(Arc::new(LargeBinaryArray::from(bufs))) + } + DataType::FixedSizeBinary(byte_width) => { + let mut builder = FixedSizeBinaryBuilder::with_capacity(values.len(), *byte_width); + for v in values { + match v { + Value::Bytes(Some(b)) => builder.append_value(b.as_slice()).map_err(|e| { + ArrowError::Unsupported(format!("FixedSizeBinary append error: {e}")) + })?, + _ => builder.append_null(), + } + } + Ok(Arc::new(builder.finish())) + } + DataType::Date32 => { + let arr: Date32Array = values.iter().map(extract_date32).collect(); + Ok(Arc::new(arr)) + } + DataType::Time32(unit) => { + let vals: Vec> = values.iter().map(|v| extract_time32(v, unit)).collect(); + let arr: Arc = match unit { + TimeUnit::Second => Arc::new(Time32SecondArray::from(vals)), + TimeUnit::Millisecond => Arc::new(Time32MillisecondArray::from(vals)), + _ => { + return Err(ArrowError::Unsupported(format!( + "Unsupported Time32 unit: {unit:?}" + ))); + } + }; + Ok(arr) + } + DataType::Time64(unit) => { + let vals: Vec> = values.iter().map(|v| extract_time64(v, unit)).collect(); + let arr: Arc = match unit { + TimeUnit::Microsecond => Arc::new(Time64MicrosecondArray::from(vals)), + TimeUnit::Nanosecond => Arc::new(Time64NanosecondArray::from(vals)), + _ => { + return Err(ArrowError::Unsupported(format!( + "Unsupported Time64 unit: {unit:?}" + ))); + } + }; + Ok(arr) + } + DataType::Timestamp(unit, tz) => { + let vals: Vec> = + values.iter().map(|v| extract_timestamp(v, unit)).collect(); + let arr: Arc = match unit { + TimeUnit::Second => { + let mut a = TimestampSecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + TimeUnit::Millisecond => { + let mut a = TimestampMillisecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + TimeUnit::Microsecond => { + let mut a = TimestampMicrosecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + TimeUnit::Nanosecond => { + let mut a = TimestampNanosecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + }; + Ok(arr) + } + DataType::Decimal64(precision, scale) => { + let arr: Decimal64Array = values + .iter() + .map(|v| extract_decimal64(v, *scale)) + .collect(); + let arr = arr + .with_precision_and_scale(*precision, *scale) + .map_err(|e| { + ArrowError::Unsupported(format!("Invalid Decimal64 precision/scale: {e}")) + })?; + Ok(Arc::new(arr)) + } + DataType::Decimal128(precision, scale) => { + let arr: Decimal128Array = values + .iter() + .map(|v| extract_decimal128(v, *scale)) + .collect(); + let arr = arr + .with_precision_and_scale(*precision, *scale) + .map_err(|e| { + ArrowError::Unsupported(format!("Invalid Decimal128 precision/scale: {e}")) + })?; + Ok(Arc::new(arr)) + } + DataType::Decimal256(precision, scale) => { + let arr: Decimal256Array = values + .iter() + .map(|v| extract_decimal256(v, *scale)) + .collect(); + let arr = arr + .with_precision_and_scale(*precision, *scale) + .map_err(|e| { + ArrowError::Unsupported(format!("Invalid Decimal256 precision/scale: {e}")) + })?; + Ok(Arc::new(arr)) + } + _ => Err(ArrowError::Unsupported(format!( + "Unsupported Arrow DataType for to_arrow: {data_type:?}" + ))), + } +} + +/// Convert a slice of optional [`Value`]s to an Arrow array matching the +/// target [`DataType`](arrow::datatypes::DataType). +/// +/// `None` entries (from `ActiveValue::NotSet`) become null in the array. +/// `Some(Value::Variant(None))` (SQL NULL) also become null. +pub fn option_values_to_arrow_array( + values: &[Option], + data_type: &arrow::datatypes::DataType, +) -> Result, ArrowError> { + use arrow::datatypes::{DataType, TimeUnit}; + use std::sync::Arc; + + match data_type { + DataType::Int8 => { + let arr: Int8Array = values + .iter() + .map(|v| match v { + Some(Value::TinyInt(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Int16 => { + let arr: Int16Array = values + .iter() + .map(|v| match v { + Some(Value::SmallInt(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Int32 => { + let arr: Int32Array = values + .iter() + .map(|v| match v { + Some(Value::Int(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Int64 => { + let arr: Int64Array = values + .iter() + .map(|v| match v { + Some(Value::BigInt(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt8 => { + let arr: UInt8Array = values + .iter() + .map(|v| match v { + Some(Value::TinyUnsigned(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt16 => { + let arr: UInt16Array = values + .iter() + .map(|v| match v { + Some(Value::SmallUnsigned(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt32 => { + let arr: UInt32Array = values + .iter() + .map(|v| match v { + Some(Value::Unsigned(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::UInt64 => { + let arr: UInt64Array = values + .iter() + .map(|v| match v { + Some(Value::BigUnsigned(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Float32 => { + let arr: Float32Array = values + .iter() + .map(|v| match v { + Some(Value::Float(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Float64 => { + let arr: Float64Array = values + .iter() + .map(|v| match v { + Some(Value::Double(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Boolean => { + let arr: BooleanArray = values + .iter() + .map(|v| match v { + Some(Value::Bool(inner)) => *inner, + _ => None, + }) + .collect(); + Ok(Arc::new(arr)) + } + DataType::Utf8 => { + let strs: Vec> = values + .iter() + .map(|v| match v { + Some(Value::String(Some(s))) => Some(s.as_str()), + _ => None, + }) + .collect(); + Ok(Arc::new(StringArray::from(strs))) + } + DataType::LargeUtf8 => { + let strs: Vec> = values + .iter() + .map(|v| match v { + Some(Value::String(Some(s))) => Some(s.as_str()), + _ => None, + }) + .collect(); + Ok(Arc::new(LargeStringArray::from(strs))) + } + DataType::Binary => { + let bufs: Vec> = values + .iter() + .map(|v| match v { + Some(Value::Bytes(Some(b))) => Some(b.as_slice()), + _ => None, + }) + .collect(); + Ok(Arc::new(BinaryArray::from(bufs))) + } + DataType::LargeBinary => { + let bufs: Vec> = values + .iter() + .map(|v| match v { + Some(Value::Bytes(Some(b))) => Some(b.as_slice()), + _ => None, + }) + .collect(); + Ok(Arc::new(LargeBinaryArray::from(bufs))) + } + DataType::FixedSizeBinary(byte_width) => { + let mut builder = FixedSizeBinaryBuilder::with_capacity(values.len(), *byte_width); + for v in values { + match v { + Some(Value::Bytes(Some(b))) => { + builder.append_value(b.as_slice()).map_err(|e| { + ArrowError::Unsupported(format!("FixedSizeBinary append error: {e}")) + })? + } + _ => builder.append_null(), + } + } + Ok(Arc::new(builder.finish())) + } + DataType::Date32 => { + let arr: Date32Array = values.iter().map(extract_date32_option).collect(); + Ok(Arc::new(arr)) + } + DataType::Time32(unit) => { + let vals: Vec> = values + .iter() + .map(|v| extract_time32_option(v, unit)) + .collect(); + let arr: Arc = match unit { + TimeUnit::Second => Arc::new(Time32SecondArray::from(vals)), + TimeUnit::Millisecond => Arc::new(Time32MillisecondArray::from(vals)), + _ => { + return Err(ArrowError::Unsupported(format!( + "Unsupported Time32 unit: {unit:?}" + ))); + } + }; + Ok(arr) + } + DataType::Time64(unit) => { + let vals: Vec> = values + .iter() + .map(|v| extract_time64_option(v, unit)) + .collect(); + let arr: Arc = match unit { + TimeUnit::Microsecond => Arc::new(Time64MicrosecondArray::from(vals)), + TimeUnit::Nanosecond => Arc::new(Time64NanosecondArray::from(vals)), + _ => { + return Err(ArrowError::Unsupported(format!( + "Unsupported Time64 unit: {unit:?}" + ))); + } + }; + Ok(arr) + } + DataType::Timestamp(unit, tz) => { + let vals: Vec> = values + .iter() + .map(|v| extract_timestamp_option(v, unit)) + .collect(); + let arr: Arc = match unit { + TimeUnit::Second => { + let mut a = TimestampSecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + TimeUnit::Millisecond => { + let mut a = TimestampMillisecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + TimeUnit::Microsecond => { + let mut a = TimestampMicrosecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + TimeUnit::Nanosecond => { + let mut a = TimestampNanosecondArray::from(vals); + if let Some(tz) = tz { + a = a.with_timezone(tz.as_ref()); + } + Arc::new(a) + } + }; + Ok(arr) + } + DataType::Decimal64(precision, scale) => { + let arr: Decimal64Array = values + .iter() + .map(|v| extract_decimal64_option(v, *scale)) + .collect(); + let arr = arr + .with_precision_and_scale(*precision, *scale) + .map_err(|e| { + ArrowError::Unsupported(format!("Invalid Decimal64 precision/scale: {e}")) + })?; + Ok(Arc::new(arr)) + } + DataType::Decimal128(precision, scale) => { + let arr: Decimal128Array = values + .iter() + .map(|v| extract_decimal128_option(v, *scale)) + .collect(); + let arr = arr + .with_precision_and_scale(*precision, *scale) + .map_err(|e| { + ArrowError::Unsupported(format!("Invalid Decimal128 precision/scale: {e}")) + })?; + Ok(Arc::new(arr)) + } + DataType::Decimal256(precision, scale) => { + let arr: Decimal256Array = values + .iter() + .map(|v| extract_decimal256_option(v, *scale)) + .collect(); + let arr = arr + .with_precision_and_scale(*precision, *scale) + .map_err(|e| { + ArrowError::Unsupported(format!("Invalid Decimal256 precision/scale: {e}")) + })?; + Ok(Arc::new(arr)) + } + _ => Err(ArrowError::Unsupported(format!( + "Unsupported Arrow DataType for to_arrow: {data_type:?}" + ))), + } +} + +// --------------------------------------------------------------------------- +// Date extraction helpers +// --------------------------------------------------------------------------- + +fn extract_date32_option(v: &Option) -> Option { + extract_date32(v.as_ref()?) +} + +fn extract_date32(v: &Value) -> Option { + #[cfg(feature = "with-chrono")] + if let Value::ChronoDate(Some(d)) = v { + let epoch = sea_query::prelude::chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + return Some((*d - epoch).num_days() as i32); + } + #[cfg(feature = "with-time")] + if let Value::TimeDate(Some(d)) = v { + return Some(d.to_julian_day() - 2_440_588); + } + let _ = v; + None +} + +// --------------------------------------------------------------------------- +// Time extraction helpers +// --------------------------------------------------------------------------- + +fn extract_time32_option(v: &Option, unit: &arrow::datatypes::TimeUnit) -> Option { + extract_time32(v.as_ref()?, unit) +} + +fn extract_time32(v: &Value, unit: &arrow::datatypes::TimeUnit) -> Option { + #[cfg(any(feature = "with-chrono", feature = "with-time"))] + use arrow::datatypes::TimeUnit; + + #[cfg(feature = "with-chrono")] + if let Value::ChronoTime(Some(t)) = v { + use sea_query::prelude::chrono::Timelike; + let secs = t.num_seconds_from_midnight() as i32; + return match unit { + TimeUnit::Second => Some(secs), + TimeUnit::Millisecond => { + let ms = (t.nanosecond() / 1_000_000) as i32; + Some(secs * 1_000 + ms) + } + _ => None, + }; + } + #[cfg(feature = "with-time")] + if let Value::TimeTime(Some(t)) = v { + let secs = (t.hour() as i32) * 3600 + (t.minute() as i32) * 60 + (t.second() as i32); + return match unit { + TimeUnit::Second => Some(secs), + TimeUnit::Millisecond => { + let ms = (t.nanosecond() / 1_000_000) as i32; + Some(secs * 1_000 + ms) + } + _ => None, + }; + } + let _ = (v, unit); + None +} + +fn extract_time64_option(v: &Option, unit: &arrow::datatypes::TimeUnit) -> Option { + extract_time64(v.as_ref()?, unit) +} + +fn extract_time64(v: &Value, unit: &arrow::datatypes::TimeUnit) -> Option { + #[cfg(any(feature = "with-chrono", feature = "with-time"))] + use arrow::datatypes::TimeUnit; + + #[cfg(feature = "with-chrono")] + if let Value::ChronoTime(Some(t)) = v { + use sea_query::prelude::chrono::Timelike; + let secs = t.num_seconds_from_midnight() as i64; + let nanos = (t.nanosecond() % 1_000_000_000) as i64; + return match unit { + TimeUnit::Microsecond => Some(secs * 1_000_000 + nanos / 1_000), + TimeUnit::Nanosecond => Some(secs * 1_000_000_000 + nanos), + _ => None, + }; + } + #[cfg(feature = "with-time")] + if let Value::TimeTime(Some(t)) = v { + let secs = (t.hour() as i64) * 3600 + (t.minute() as i64) * 60 + (t.second() as i64); + let nanos = t.nanosecond() as i64; + return match unit { + TimeUnit::Microsecond => Some(secs * 1_000_000 + nanos / 1_000), + TimeUnit::Nanosecond => Some(secs * 1_000_000_000 + nanos), + _ => None, + }; + } + let _ = (v, unit); + None +} + +// --------------------------------------------------------------------------- +// Timestamp extraction helpers +// --------------------------------------------------------------------------- + +fn extract_timestamp_option(v: &Option, unit: &arrow::datatypes::TimeUnit) -> Option { + extract_timestamp(v.as_ref()?, unit) +} + +fn extract_timestamp(v: &Value, unit: &arrow::datatypes::TimeUnit) -> Option { + #[cfg(any(feature = "with-chrono", feature = "with-time"))] + use arrow::datatypes::TimeUnit; + + #[cfg(feature = "with-chrono")] + { + if let Value::ChronoDateTime(Some(dt)) = v { + let utc = dt.and_utc(); + return Some(match unit { + TimeUnit::Second => utc.timestamp(), + TimeUnit::Millisecond => utc.timestamp_millis(), + TimeUnit::Microsecond => utc.timestamp_micros(), + TimeUnit::Nanosecond => utc.timestamp_nanos_opt().unwrap_or(0), + }); + } + if let Value::ChronoDateTimeUtc(Some(dt)) = v { + return Some(match unit { + TimeUnit::Second => dt.timestamp(), + TimeUnit::Millisecond => dt.timestamp_millis(), + TimeUnit::Microsecond => dt.timestamp_micros(), + TimeUnit::Nanosecond => dt.timestamp_nanos_opt().unwrap_or(0), + }); + } + } + #[cfg(feature = "with-time")] + { + if let Value::TimeDateTime(Some(dt)) = v { + let odt = dt.assume_utc(); + return Some(offset_dt_to_timestamp(&odt, unit)); + } + if let Value::TimeDateTimeWithTimeZone(Some(dt)) = v { + return Some(offset_dt_to_timestamp(dt, unit)); + } + } + let _ = (v, unit); + None +} + +#[cfg(feature = "with-time")] +fn offset_dt_to_timestamp( + dt: &sea_query::prelude::time::OffsetDateTime, + unit: &arrow::datatypes::TimeUnit, +) -> i64 { + use arrow::datatypes::TimeUnit; + match unit { + TimeUnit::Second => dt.unix_timestamp(), + TimeUnit::Millisecond => (dt.unix_timestamp_nanos() / 1_000_000) as i64, + TimeUnit::Microsecond => (dt.unix_timestamp_nanos() / 1_000) as i64, + TimeUnit::Nanosecond => dt.unix_timestamp_nanos() as i64, + } +} + +// --------------------------------------------------------------------------- +// Decimal extraction helpers +// --------------------------------------------------------------------------- + +fn extract_decimal64_option(v: &Option, target_scale: i8) -> Option { + extract_decimal64(v.as_ref()?, target_scale) +} + +fn extract_decimal64(v: &Value, target_scale: i8) -> Option { + #[cfg(feature = "with-rust_decimal")] + if let Value::Decimal(Some(d)) = v { + let mantissa = d.mantissa(); + let current_scale = d.scale() as i8; + let scale_diff = target_scale - current_scale; + let scaled = if scale_diff >= 0 { + mantissa * 10i128.pow(scale_diff as u32) + } else { + mantissa / 10i128.pow((-scale_diff) as u32) + }; + return i64::try_from(scaled).ok(); + } + #[cfg(feature = "with-bigdecimal")] + if let Value::BigDecimal(Some(d)) = v { + return bigdecimal_to_i64(d, target_scale); + } + let _ = (v, target_scale); + None +} + +#[cfg(feature = "with-bigdecimal")] +fn bigdecimal_to_i64( + d: &sea_query::prelude::bigdecimal::BigDecimal, + target_scale: i8, +) -> Option { + use sea_query::prelude::bigdecimal::ToPrimitive; + + let rescaled = d.clone().with_scale(target_scale as i64); + let (digits, _) = rescaled.into_bigint_and_exponent(); + digits.to_i64() +} + +fn extract_decimal128_option(v: &Option, target_scale: i8) -> Option { + extract_decimal128(v.as_ref()?, target_scale) +} + +fn extract_decimal128(v: &Value, target_scale: i8) -> Option { + #[cfg(feature = "with-rust_decimal")] + if let Value::Decimal(Some(d)) = v { + let mantissa = d.mantissa(); + let current_scale = d.scale() as i8; + let scale_diff = target_scale - current_scale; + return if scale_diff >= 0 { + Some(mantissa * 10i128.pow(scale_diff as u32)) + } else { + Some(mantissa / 10i128.pow((-scale_diff) as u32)) + }; + } + #[cfg(feature = "with-bigdecimal")] + if let Value::BigDecimal(Some(d)) = v { + return bigdecimal_to_i128(d, target_scale); + } + let _ = (v, target_scale); + None +} + +#[cfg(feature = "with-bigdecimal")] +fn bigdecimal_to_i128( + d: &sea_query::prelude::bigdecimal::BigDecimal, + target_scale: i8, +) -> Option { + use sea_query::prelude::bigdecimal::ToPrimitive; + + let rescaled = d.clone().with_scale(target_scale as i64); + let (digits, _) = rescaled.into_bigint_and_exponent(); + digits.to_i128() +} + +fn extract_decimal256_option(v: &Option, target_scale: i8) -> Option { + extract_decimal256(v.as_ref()?, target_scale) +} + +fn extract_decimal256(v: &Value, target_scale: i8) -> Option { + #[cfg(feature = "with-bigdecimal")] + if let Value::BigDecimal(Some(d)) = v { + return bigdecimal_to_i256(d, target_scale); + } + #[cfg(feature = "with-rust_decimal")] + if let Value::Decimal(Some(d)) = v { + let mantissa = d.mantissa(); + let current_scale = d.scale() as i8; + let scale_diff = target_scale - current_scale; + let scaled = if scale_diff >= 0 { + mantissa * 10i128.pow(scale_diff as u32) + } else { + mantissa / 10i128.pow((-scale_diff) as u32) + }; + return Some(i256::from_i128(scaled)); + } + let _ = (v, target_scale); + None +} + +#[cfg(feature = "with-bigdecimal")] +fn bigdecimal_to_i256( + d: &sea_query::prelude::bigdecimal::BigDecimal, + target_scale: i8, +) -> Option { + let rescaled = d.clone().with_scale(target_scale as i64); + let (digits, _) = rescaled.into_bigint_and_exponent(); + bigint_to_i256(&digits) +} + +#[cfg(feature = "with-bigdecimal")] +fn bigint_to_i256(bi: &sea_query::prelude::bigdecimal::num_bigint::BigInt) -> Option { + use sea_query::prelude::bigdecimal::num_bigint::Sign; + + let (sign, bytes) = bi.to_bytes_be(); + if bytes.len() > 32 { + return None; + } + + let mut buf = [0u8; 32]; + let start = 32 - bytes.len(); + buf[start..].copy_from_slice(&bytes); + + let val = i256::from_be_bytes(buf); + match sign { + Sign::Minus => Some(val.wrapping_neg()), + _ => Some(val), + } +} diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index e40061eed2..c1394cbcf8 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -2,61 +2,131 @@ # A separate workspace [package] -name = "sea-orm-cli" -version = "0.12.0" authors = [ "Chris Tsang ", "Billy Chan ", ] -edition = "2021" +categories = ["database"] +default-run = "sea-orm-cli" description = "Command line utility for SeaORM" -license = "MIT OR Apache-2.0" -homepage = "https://www.sea-ql.org/SeaORM" documentation = "https://docs.rs/sea-orm" -repository = "https://github.com/SeaQL/sea-orm" -categories = ["database"] +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" keywords = ["async", "orm", "mysql", "postgres", "sqlite"] -default-run = "sea-orm-cli" -rust-version = "1.65" +license = "MIT OR Apache-2.0" +name = "sea-orm-cli" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.37" [lib] name = "sea_orm_cli" path = "src/lib.rs" [[bin]] -name = "sea-orm-cli" -path = "src/bin/main.rs" +name = "sea-orm-cli" +path = "src/bin/main.rs" required-features = ["cli", "codegen"] [[bin]] -name = "sea" -path = "src/bin/sea.rs" +name = "sea" +path = "src/bin/main.rs" required-features = ["cli", "codegen"] [dependencies] -clap = { version = "3.2", default-features = false, features = ["std", "env", "derive"], optional = true } +async-std = { version = "1.9", default-features = false, features = [ + "attributes", + "tokio1", +], optional = true } +chrono = { version = "0.4.20", default-features = false, features = [ + "clock", +] } +clap = { version = "4.3", features = ["env", "derive"], optional = true } dotenvy = { version = "0.15", default-features = false, optional = true } -async-std = { version = "1.9", default-features = false, features = ["attributes", "tokio1"], optional = true } -sea-orm-codegen = { version = "=0.12.0", path = "../sea-orm-codegen", default-features = false, optional = true } -sea-schema = { version = "0.12.0-rc.2" } -sqlx = { version = "0.6", default-features = false, features = ["mysql", "postgres"], optional = true } -tracing-subscriber = { version = "0.3.17", default-features = false, features = ["env-filter", "fmt"] } +glob = { version = "0.3", default-features = false } +indoc = "2.0.6" +regex = { version = "1.11.2" } +sea-orm-codegen = { version = "=2.0.0-rc.37", path = "../sea-orm-codegen", default-features = false, optional = true } +sea-schema = { version = "0.17.0-rc.1", default-features = false, features = [ + "discovery", + "writer", + "probe", +], optional = true } +sqlx = { version = "0.8.4", default-features = false, optional = true } +tokio = { version = "1.38.2", default-features = false, features = [ + "rt-multi-thread", + "macros", +], optional = true } tracing = { version = "0.1", default-features = false } +tracing-subscriber = { version = "0.3.17", default-features = false, features = [ + "env-filter", + "fmt", +] } url = { version = "2.2", default-features = false } -chrono = { version = "0.4.20", default-features = false, features = ["clock"] } -regex = { version = "1", default-features = false } -git2 = { version = "0.16", default-features = false } [dev-dependencies] smol = "1.2.5" [features] -default = ["codegen", "cli", "runtime-async-std-native-tls", "async-std"] -codegen = ["sea-schema/sqlx-all", "sea-orm-codegen"] cli = ["clap", "dotenvy"] -runtime-actix-native-tls = ["sqlx/runtime-actix-native-tls", "sea-schema/runtime-actix-native-tls"] -runtime-async-std-native-tls = ["sqlx/runtime-async-std-native-tls", "sea-schema/runtime-async-std-native-tls"] -runtime-tokio-native-tls = ["sqlx/runtime-tokio-native-tls", "sea-schema/runtime-tokio-native-tls"] -runtime-actix-rustls = ["sqlx/runtime-actix-rustls", "sea-schema/runtime-actix-rustls"] -runtime-async-std-rustls = ["sqlx/runtime-async-std-rustls", "sea-schema/runtime-async-std-rustls"] -runtime-tokio-rustls = ["sqlx/runtime-tokio-rustls", "sea-schema/runtime-tokio-rustls"] +codegen = ["cli", "sqlx", "sea-schema", "sea-orm-codegen"] +default = [ + "codegen", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "runtime-tokio-native-tls", +] +postgres-vector = ["sea-schema/postgres-vector"] +sqlx-mysql = [ + "sqlx?/sqlx-mysql", + "sea-schema?/sqlx-mysql", + "sea-schema?/mysql", +] +sqlx-postgres = [ + "sqlx?/sqlx-postgres", + "sea-schema?/sqlx-postgres", + "sea-schema?/postgres", +] +sqlx-sqlite = [ + "sqlx?/sqlx-sqlite", + "sea-schema?/sqlx-sqlite", + "sea-schema?/sqlite", +] + +runtime-actix = ["runtime-tokio"] +runtime-actix-native-tls = ["runtime-tokio-native-tls"] +runtime-actix-rustls = ["runtime-tokio-rustls"] + +runtime-async-std = [ + "async-std", + "sqlx?/runtime-async-std", + "sea-schema?/runtime-async-std", +] +runtime-async-std-native-tls = [ + "async-std", + "sqlx?/runtime-async-std-native-tls", + "sea-schema?/runtime-async-std-native-tls", +] +runtime-async-std-rustls = [ + "async-std", + "sqlx?/runtime-async-std-rustls", + "sea-schema?/runtime-async-std-rustls", +] + +runtime-tokio = ["tokio", "sqlx?/runtime-tokio", "sea-schema?/runtime-tokio"] +runtime-tokio-native-tls = [ + "tokio", + "sqlx?/runtime-tokio-native-tls", + "sea-schema?/runtime-tokio-native-tls", +] +runtime-tokio-rustls = [ + "tokio", + "sqlx?/runtime-tokio-rustls", + "sea-schema?/runtime-tokio-rustls", +] + +# This allows us to develop using an overridden version of sea-query +[patch.crates-io] +# sea-query = { path = "../sea-query" } +# sea-query = { git = "https://github.com/SeaQL/sea-query", branch = "master" } diff --git a/sea-orm-cli/src/bin/main.rs b/sea-orm-cli/src/bin/main.rs index e847d003f1..35daa00026 100644 --- a/sea-orm-cli/src/bin/main.rs +++ b/sea-orm-cli/src/bin/main.rs @@ -1,32 +1,19 @@ -use clap::StructOpt; -use dotenvy::dotenv; -use sea_orm_cli::{handle_error, run_generate_command, run_migrate_command, Cli, Commands}; - -#[async_std::main] +#[cfg_attr( + any( + feature = "runtime-async-std", + feature = "runtime-async-std-native-tls", + feature = "runtime-async-std-rustls" + ), + async_std::main +)] +#[cfg_attr( + any( + feature = "runtime-tokio", + feature = "runtime-tokio-native-tls", + feature = "runtime-tokio-rustls", + ), + tokio::main +)] async fn main() { - dotenv().ok(); - - let cli = Cli::parse(); - let verbose = cli.verbose; - - match cli.command { - Commands::Generate { command } => { - run_generate_command(command, verbose) - .await - .unwrap_or_else(handle_error); - } - Commands::Migrate { - migration_dir, - database_schema, - database_url, - command, - } => run_migrate_command( - command, - &migration_dir, - database_schema, - database_url, - verbose, - ) - .unwrap_or_else(handle_error), - } + sea_orm_cli::main().await } diff --git a/sea-orm-cli/src/bin/sea.rs b/sea-orm-cli/src/bin/sea.rs deleted file mode 100644 index edf15c8315..0000000000 --- a/sea-orm-cli/src/bin/sea.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! COPY FROM bin/main.rs - -use clap::StructOpt; -use dotenvy::dotenv; -use sea_orm_cli::{handle_error, run_generate_command, run_migrate_command, Cli, Commands}; - -#[async_std::main] -async fn main() { - dotenv().ok(); - - let cli = Cli::parse(); - let verbose = cli.verbose; - - match cli.command { - Commands::Generate { command } => { - run_generate_command(command, verbose) - .await - .unwrap_or_else(handle_error); - } - Commands::Migrate { - migration_dir, - database_schema, - database_url, - command, - } => run_migrate_command( - command, - &migration_dir, - database_schema, - database_url, - verbose, - ) - .unwrap_or_else(handle_error), - } -} diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index d37c5ca085..810a81ba66 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -1,7 +1,13 @@ -use clap::{ArgEnum, ArgGroup, Parser, Subcommand}; +use clap::{ArgAction, ArgGroup, Parser, Subcommand, ValueEnum}; +#[cfg(feature = "codegen")] +use dotenvy::dotenv; +use std::ffi::OsStr; + +#[cfg(feature = "codegen")] +use crate::{handle_error, run_generate_command, run_migrate_command}; #[derive(Parser, Debug)] -#[clap( +#[command( version, author, help_template = r#"{before-help}{name} {version} @@ -11,8 +17,6 @@ use clap::{ArgEnum, ArgGroup, Parser, Subcommand}; {all-args}{after-help} -AUTHORS: - {author} "#, about = r#" ____ ___ ____ __ __ /\ @@ -24,44 +28,48 @@ AUTHORS: An async & dynamic ORM for Rust \ | ,/ =============================== \|_/ - Getting Started! - - documentation: https://www.sea-ql.org/SeaORM - - step-by-step tutorials: https://www.sea-ql.org/sea-orm-tutorial - - integration examples: https://github.com/SeaQL/sea-orm/tree/master/examples - - cookbook: https://www.sea-ql.org/sea-orm-cookbook + Getting Started + - Documentation: https://www.sea-ql.org/SeaORM + - Tutorial: https://www.sea-ql.org/sea-orm-tutorial + - Examples: https://github.com/SeaQL/sea-orm/tree/master/examples + - Cookbook: https://www.sea-ql.org/sea-orm-cookbook Join our Discord server to chat with others in the SeaQL community! - - invitation link: https://discord.com/invite/uCPdDXzbdv + - Invitation: https://discord.com/invite/uCPdDXzbdv + + SeaQL Community Survey 2025 + - Link: https://www.sea-ql.org/community-survey/ - If you like what we do, consider starring, commenting, sharing and contributing! + If you like what we do, consider starring, sharing and contributing! "# )] pub struct Cli { - #[clap(action, global = true, short, long, help = "Show debug messages")] + #[arg(global = true, short, long, help = "Show debug messages")] pub verbose: bool, - #[clap(subcommand)] + #[command(subcommand)] pub command: Commands, } +#[allow(clippy::large_enum_variant)] #[derive(Subcommand, PartialEq, Eq, Debug)] pub enum Commands { - #[clap( + #[command( about = "Codegen related commands", arg_required_else_help = true, display_order = 10 )] Generate { - #[clap(subcommand)] + #[command(subcommand)] command: GenerateSubcommands, }, - #[clap(about = "Migration related commands", display_order = 20)] + #[command(about = "Migration related commands", display_order = 20)] Migrate { - #[clap( - value_parser, + #[arg( global = true, short = 'd', long, + env = "MIGRATION_DIR", help = "Migration script directory. If your migrations are in their own crate, you can provide the root of that crate. @@ -71,8 +79,7 @@ you should provide the directory of that submodule.", )] migration_dir: String, - #[clap( - value_parser, + #[arg( global = true, short = 's', long, @@ -83,86 +90,69 @@ you should provide the directory of that submodule.", )] database_schema: Option, - #[clap( - value_parser, + #[arg( global = true, short = 'u', long, env = "DATABASE_URL", - help = "Database URL" + help = "Database URL", + hide_env_values = true )] database_url: Option, - #[clap(subcommand)] + #[command(subcommand)] command: Option, }, } #[derive(Subcommand, PartialEq, Eq, Debug)] pub enum MigrateSubcommands { - #[clap(about = "Initialize migration directory", display_order = 10)] + #[command(about = "Initialize migration directory", display_order = 10)] Init, - #[clap(about = "Generate a new, empty migration", display_order = 20)] + #[command(about = "Generate a new, empty migration", display_order = 20)] Generate { - #[clap( - value_parser, - required = true, - takes_value = true, - help = "Name of the new migration" - )] + #[arg(required = true, help = "Name of the new migration")] migration_name: String, - #[clap( - action, + #[arg( long, default_value = "true", help = "Generate migration file based on Utc time", - conflicts_with = "local-time", + conflicts_with = "local_time", display_order = 1001 )] universal_time: bool, - #[clap( - action, + #[arg( long, help = "Generate migration file based on Local time", - conflicts_with = "universal-time", + conflicts_with = "universal_time", display_order = 1002 )] local_time: bool, }, - #[clap( + #[command( about = "Drop all tables from the database, then reapply all migrations", display_order = 30 )] Fresh, - #[clap( + #[command( about = "Rollback all applied migrations, then reapply all migrations", display_order = 40 )] Refresh, - #[clap(about = "Rollback all applied migrations", display_order = 50)] + #[command(about = "Rollback all applied migrations", display_order = 50)] Reset, - #[clap(about = "Check the status of all migrations", display_order = 60)] + #[command(about = "Check the status of all migrations", display_order = 60)] Status, - #[clap(about = "Apply pending migrations", display_order = 70)] + #[command(about = "Apply pending migrations", display_order = 70)] Up { - #[clap( - value_parser, - short, - long, - help = "Number of pending migrations to apply" - )] + #[arg(short, long, help = "Number of pending migrations to apply")] num: Option, }, - #[clap( - value_parser, - about = "Rollback applied migrations", - display_order = 80 - )] + #[command(about = "Rollback applied migrations", display_order = 80)] Down { - #[clap( - value_parser, + #[arg( short, long, default_value = "1", @@ -175,54 +165,59 @@ pub enum MigrateSubcommands { #[derive(Subcommand, PartialEq, Eq, Debug)] pub enum GenerateSubcommands { - #[clap(about = "Generate entity")] - #[clap(arg_required_else_help = true)] - #[clap(group(ArgGroup::new("formats").args(&["compact-format", "expanded-format"])))] - #[clap(group(ArgGroup::new("group-tables").args(&["tables", "include-hidden-tables"])))] + #[command(about = "Generate entity")] + #[command(group(ArgGroup::new("formats").args(&["compact_format", "expanded_format", "frontend_format"])))] + #[command(group(ArgGroup::new("group-tables").args(&["tables", "include_hidden_tables"])))] Entity { - #[clap(action, long, help = "Generate entity file of compact format")] + #[arg(long, help = "Which format to generate entity files in")] + entity_format: Option, + + #[arg(long, help = "Generate entity file of compact format")] compact_format: bool, - #[clap(action, long, help = "Generate entity file of expanded format")] + #[arg(long, help = "Generate entity file of expanded format")] expanded_format: bool, - #[clap( - action, + #[arg(long, help = "Generate entity file of frontend format")] + frontend_format: bool, + + #[arg( long, help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)" )] include_hidden_tables: bool, - #[clap( - value_parser, + #[arg( short = 't', long, - use_value_delimiter = true, - takes_value = true, + value_delimiter = ',', help = "Generate entity file for specified tables only (comma separated)" )] tables: Vec, - #[clap( - value_parser, + #[arg( long, - use_value_delimiter = true, - takes_value = true, + value_delimiter = ',', default_value = "seaql_migrations", help = "Skip generating entity file for specified tables (comma separated)" )] ignore_tables: Vec, - #[clap( - value_parser, + #[arg( long, default_value = "1", help = "The maximum amount of connections to use when connecting to the database." )] max_connections: u32, - #[clap( - value_parser, + #[arg( + long, + default_value = "30", + long_help = "Acquire timeout in seconds of the connection used for schema discovery" + )] + acquire_timeout: u64, + + #[arg( short = 'o', long, default_value = "./", @@ -230,29 +225,33 @@ pub enum GenerateSubcommands { )] output_dir: String, - #[clap( - value_parser, + #[arg( short = 's', long, env = "DATABASE_SCHEMA", - default_value = "public", long_help = "Database schema\n \ - For MySQL, this argument is ignored.\n \ - For PostgreSQL, this argument is optional with default value 'public'." )] - database_schema: String, + database_schema: Option, - #[clap( - value_parser, + #[arg( short = 'u', long, env = "DATABASE_URL", - help = "Database URL" + help = "Database URL", + hide_env_values = true )] database_url: String, - #[clap( - value_parser, + #[arg( + long, + default_value = "all", + help = "Generate prelude.rs file (all, none, all-allow-unused-imports)" + )] + with_prelude: String, + + #[arg( long, default_value = "none", help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, \ @@ -260,23 +259,20 @@ pub enum GenerateSubcommands { )] with_serde: String, - #[clap( - action, + #[arg( long, help = "Generate a serde field attribute, '#[serde(skip_deserializing)]', for the primary key fields to skip them during deserialization, this flag will be affective only when '--with-serde' is 'both' or 'deserialize'" )] serde_skip_deserializing_primary_key: bool, - #[clap( - action, + #[arg( long, default_value = "false", help = "Opt-in to add skip attributes to hidden columns (i.e. when 'with-serde' enabled and column name starts with an underscore)" )] serde_skip_hidden_column: bool, - #[clap( - action, + #[arg( long, default_value = "false", long_help = "Automatically derive the Copy trait on generated enums.\n\ @@ -286,17 +282,23 @@ pub enum GenerateSubcommands { )] with_copy_enums: bool, - #[clap( - arg_enum, - value_parser, + #[arg( long, - default_value = "chrono", + default_value_t, + value_enum, help = "The datetime crate to use for generating entities." )] date_time_crate: DateTimeCrate, - #[clap( - action, + #[arg( + long, + default_value_t, + value_enum, + help = "The primitive type to use for big integer." + )] + big_integer_type: BigIntegerType, + + #[arg( long, short = 'l', default_value = "false", @@ -304,36 +306,158 @@ pub enum GenerateSubcommands { )] lib: bool, - #[clap( - value_parser, + #[arg( long, - use_value_delimiter = true, - takes_value = true, + value_delimiter = ',', help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`" )] model_extra_derives: Vec, - #[clap( - value_parser, + #[arg( long, - use_value_delimiter = true, - takes_value = true, + value_delimiter = ',', help = r#"Add extra attributes to generated model struct, no need for `#[]` (comma separated), e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"# )] model_extra_attributes: Vec, - #[clap( - action, + #[arg( + long, + value_delimiter = ',', + help = "Add extra derive macros to generated enums (comma separated), e.g. `--enum-extra-derives 'ts_rs::Ts','CustomDerive'`" + )] + enum_extra_derives: Vec, + + #[arg( + long, + value_delimiter = ',', + help = r#"Add extra attributes to generated enums, no need for `#[]` (comma separated), e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"# + )] + enum_extra_attributes: Vec, + + #[arg( + long, + value_delimiter = ',', + help = "Add extra derive macros to generated column enum (comma separated), e.g. `--column-extra-derives 'async_graphql::Enum','CustomDerive'`" + )] + column_extra_derives: Vec, + + #[arg( long, default_value = "false", long_help = "Generate helper Enumerations that are used by Seaography." )] seaography: bool, + + #[arg( + long, + default_value = "true", + default_missing_value = "true", + num_args = 0..=1, + require_equals = true, + action = ArgAction::Set, + long_help = "Generate empty ActiveModelBehavior impls." + )] + impl_active_model_behavior: bool, + + #[arg( + long = "experimental-preserve-user-modifications", + alias = "preserve-user-modifications", + default_value = "false", + default_missing_value = "true", + num_args = 0..=1, + require_equals = true, + action = ArgAction::Set, + long_help = indoc::indoc! { " + Experimental!: Preserve user modifications when regenerating entity files. + Only supports: + - Extra derives and attributes of `Model` and `Relation` + - Impl blocks of `ActiveModelBehavior` + Deprecated alias: `--preserve-user-modifications`" + } + )] + preserve_user_modifications: bool, + + #[arg( + long, + default_value_t, + value_enum, + help = "Control how the codegen version is displayed in the top banner of the generated file." + )] + banner_version: BannerVersion, + + #[arg( + long, + default_value = "false", + help = "Also generate a Mermaid ER diagram as `entities.mermaid` in the output directory" + )] + er_diagram: bool, }, } -#[derive(ArgEnum, Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)] pub enum DateTimeCrate { + #[default] Chrono, Time, } + +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)] +pub enum BigIntegerType { + #[default] + I64, + I32, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)] +pub enum BannerVersion { + Off, + Major, + #[default] + Minor, + Patch, +} + +fn is_deprecated_preserve_user_modifications_flag(arg: &OsStr) -> bool { + arg.to_str() + .is_some_and(|arg| arg.starts_with("--preserve-user-modifications")) +} + +/// Use this to build a local, version-controlled `sea-orm-cli` in dependent projects +/// (see [example use case](https://github.com/SeaQL/sea-orm/discussions/1889)). +#[cfg(feature = "codegen")] +pub async fn main() { + dotenv().ok(); + + let deprecated_preserve_user_modifications_flag_used = std::env::args_os() + .skip(1) + .any(|arg| is_deprecated_preserve_user_modifications_flag(&arg)); + + let cli = Cli::parse(); + if deprecated_preserve_user_modifications_flag_used { + eprintln!( + "warning: `--preserve-user-modifications` is deprecated; use `--experimental-preserve-user-modifications` instead." + ); + } + let verbose = cli.verbose; + + match cli.command { + Commands::Generate { command } => { + run_generate_command(command, verbose) + .await + .unwrap_or_else(handle_error); + } + Commands::Migrate { + migration_dir, + database_schema, + database_url, + command, + } => run_migrate_command( + command, + &migration_dir, + database_schema, + database_url, + verbose, + ) + .unwrap_or_else(handle_error), + } +} diff --git a/sea-orm-cli/src/commands/generate.rs b/sea-orm-cli/src/commands/generate.rs index af6d9f7b70..7e9c5916e2 100644 --- a/sea-orm-cli/src/commands/generate.rs +++ b/sea-orm-cli/src/commands/generate.rs @@ -1,37 +1,50 @@ +use crate::{BannerVersion, BigIntegerType, DateTimeCrate, GenerateSubcommands}; +use core::time; use sea_orm_codegen::{ - DateTimeCrate as CodegenDateTimeCrate, EntityTransformer, EntityWriterContext, OutputFile, - WithSerde, + BannerVersion as CodegenBannerVersion, BigIntegerType as CodegenBigIntegerType, + DateTimeCrate as CodegenDateTimeCrate, EntityFormat, EntityTransformer, EntityWriterContext, + MergeReport, OutputFile, WithPrelude, WithSerde, merge_entity_files, }; -use std::{error::Error, fs, io::Write, path::Path, process::Command, str::FromStr}; -use tracing_subscriber::{prelude::*, EnvFilter}; +use std::{error::Error, fs, path::Path, process::Command, str::FromStr}; +use tracing_subscriber::{EnvFilter, prelude::*}; use url::Url; -use crate::{DateTimeCrate, GenerateSubcommands}; - pub async fn run_generate_command( command: GenerateSubcommands, verbose: bool, ) -> Result<(), Box> { match command { GenerateSubcommands::Entity { + entity_format, compact_format: _, expanded_format, + frontend_format, include_hidden_tables, tables, ignore_tables, max_connections, + acquire_timeout, output_dir, database_schema, database_url, + with_prelude, with_serde, serde_skip_deserializing_primary_key, serde_skip_hidden_column, with_copy_enums, date_time_crate, + big_integer_type, lib, model_extra_derives, model_extra_attributes, + enum_extra_derives, + enum_extra_attributes, + column_extra_derives, seaography, + impl_active_model_behavior, + preserve_user_modifications, + banner_version, + er_diagram, } => { if verbose { let _ = tracing_subscriber::fmt() @@ -75,7 +88,7 @@ pub async fn run_generate_command( let filter_skip_tables = |table: &String| -> bool { !ignore_tables.contains(table) }; - let database_name = if !is_sqlite { + let _database_name = if !is_sqlite { // The database name should be the first element of the path string // // Throwing an error if there is no database name since it might be @@ -107,83 +120,185 @@ pub async fn run_generate_command( let (schema_name, table_stmts) = match url.scheme() { "mysql" => { - use sea_schema::mysql::discovery::SchemaDiscovery; - use sqlx::MySql; - - let connection = connect::(max_connections, url.as_str(), None).await?; - let schema_discovery = SchemaDiscovery::new(connection, database_name); - let schema = schema_discovery.discover().await; - let table_stmts = schema - .tables - .into_iter() - .filter(|schema| filter_tables(&schema.info.name)) - .filter(|schema| filter_hidden_tables(&schema.info.name)) - .filter(|schema| filter_skip_tables(&schema.info.name)) - .map(|schema| schema.write()) - .collect(); - (None, table_stmts) + #[cfg(not(feature = "sqlx-mysql"))] + { + panic!("mysql feature is off") + } + #[cfg(feature = "sqlx-mysql")] + { + use sea_schema::mysql::discovery::SchemaDiscovery; + use sqlx::MySql; + + println!("Connecting to MySQL ..."); + let connection = sqlx_connect::( + max_connections, + acquire_timeout, + url.as_str(), + None, + ) + .await?; + println!("Discovering schema ..."); + let schema_discovery = SchemaDiscovery::new(connection, _database_name); + let schema = schema_discovery.discover().await?; + let table_stmts = schema + .tables + .into_iter() + .filter(|schema| filter_tables(&schema.info.name)) + .filter(|schema| filter_hidden_tables(&schema.info.name)) + .filter(|schema| filter_skip_tables(&schema.info.name)) + .map(|schema| schema.write()) + .collect(); + (None, table_stmts) + } } "sqlite" => { - use sea_schema::sqlite::discovery::SchemaDiscovery; - use sqlx::Sqlite; - - let connection = connect::(max_connections, url.as_str(), None).await?; - let schema_discovery = SchemaDiscovery::new(connection); - let schema = schema_discovery.discover().await?; - let table_stmts = schema - .tables - .into_iter() - .filter(|schema| filter_tables(&schema.name)) - .filter(|schema| filter_hidden_tables(&schema.name)) - .filter(|schema| filter_skip_tables(&schema.name)) - .map(|schema| schema.write()) - .collect(); - (None, table_stmts) + #[cfg(not(feature = "sqlx-sqlite"))] + { + panic!("sqlite feature is off") + } + #[cfg(feature = "sqlx-sqlite")] + { + use sea_schema::sqlite::discovery::SchemaDiscovery; + use sqlx::Sqlite; + + println!("Connecting to SQLite ..."); + let connection = sqlx_connect::( + max_connections, + acquire_timeout, + url.as_str(), + None, + ) + .await?; + println!("Discovering schema ..."); + let schema_discovery = SchemaDiscovery::new(connection); + let schema = schema_discovery + .discover() + .await? + .merge_indexes_into_table(); + let table_stmts = schema + .tables + .into_iter() + .filter(|schema| filter_tables(&schema.name)) + .filter(|schema| filter_hidden_tables(&schema.name)) + .filter(|schema| filter_skip_tables(&schema.name)) + .map(|schema| schema.write()) + .collect(); + (None, table_stmts) + } } "postgres" | "postgresql" => { - use sea_schema::postgres::discovery::SchemaDiscovery; - use sqlx::Postgres; - - let schema = &database_schema; - let connection = - connect::(max_connections, url.as_str(), Some(schema)).await?; - let schema_discovery = SchemaDiscovery::new(connection, schema); - let schema = schema_discovery.discover().await; - let table_stmts = schema - .tables - .into_iter() - .filter(|schema| filter_tables(&schema.info.name)) - .filter(|schema| filter_hidden_tables(&schema.info.name)) - .filter(|schema| filter_skip_tables(&schema.info.name)) - .map(|schema| schema.write()) - .collect(); - (Some(schema.schema), table_stmts) + #[cfg(not(feature = "sqlx-postgres"))] + { + panic!("postgres feature is off") + } + #[cfg(feature = "sqlx-postgres")] + { + use sea_schema::postgres::discovery::SchemaDiscovery; + use sqlx::Postgres; + + println!("Connecting to Postgres ..."); + let schema = database_schema.as_deref().unwrap_or("public"); + let connection = sqlx_connect::( + max_connections, + acquire_timeout, + url.as_str(), + Some(schema), + ) + .await?; + println!("Discovering schema ..."); + let schema_discovery = SchemaDiscovery::new(connection, schema); + let schema = schema_discovery.discover().await?; + let table_stmts = schema + .tables + .into_iter() + .filter(|schema| filter_tables(&schema.info.name)) + .filter(|schema| filter_hidden_tables(&schema.info.name)) + .filter(|schema| filter_skip_tables(&schema.info.name)) + .map(|schema| schema.write()) + .collect(); + (database_schema, table_stmts) + } } _ => unimplemented!("{} is not supported", url.scheme()), }; + println!("... discovered."); let writer_context = EntityWriterContext::new( - expanded_format, + if expanded_format { + EntityFormat::Expanded + } else if frontend_format { + EntityFormat::Frontend + } else if let Some(entity_format) = entity_format { + EntityFormat::from_str(&entity_format).expect("Invalid entity-format option") + } else { + EntityFormat::default() + }, + WithPrelude::from_str(&with_prelude).expect("Invalid prelude option"), WithSerde::from_str(&with_serde).expect("Invalid serde derive option"), with_copy_enums, date_time_crate.into(), + big_integer_type.into(), schema_name, lib, serde_skip_deserializing_primary_key, serde_skip_hidden_column, model_extra_derives, model_extra_attributes, + enum_extra_derives, + enum_extra_attributes, + column_extra_derives, seaography, + impl_active_model_behavior, + banner_version.into(), ); - let output = EntityTransformer::transform(table_stmts)?.generate(&writer_context); + let entity_writer = EntityTransformer::transform(table_stmts)?; let dir = Path::new(&output_dir); fs::create_dir_all(dir)?; + if er_diagram { + let diagram = entity_writer.generate_er_diagram(); + let diagram_path = dir.join("entities.mermaid"); + fs::write(&diagram_path, &diagram)?; + println!("Writing {}", diagram_path.display()); + } + + let output = entity_writer.generate(&writer_context); + + let mut merge_fallback_files: Vec = Vec::new(); + for OutputFile { name, content } in output.files.iter() { let file_path = dir.join(name); - let mut file = fs::File::create(file_path)?; - file.write_all(content.as_bytes())?; + println!("Writing {}", file_path.display()); + + if !matches!( + name.as_str(), + "mod.rs" | "lib.rs" | "prelude.rs" | "sea_orm_active_enums.rs" + ) && file_path.exists() + && preserve_user_modifications + { + let prev_content = fs::read_to_string(&file_path)?; + match merge_entity_files(&prev_content, content) { + Ok(merged) => { + fs::write(file_path, merged)?; + } + Err(MergeReport { + output, + warnings, + fallback_applied, + }) => { + for message in warnings { + eprintln!("{message}"); + } + fs::write(file_path, output)?; + if fallback_applied { + merge_fallback_files.push(name.clone()); + } + } + } + } else { + fs::write(file_path, content)?; + }; } // Format each of the files @@ -194,14 +309,26 @@ pub async fn run_generate_command( return Err(format!("Fail to format file `{name}`").into()); } } + + if merge_fallback_files.is_empty() { + println!("... Done."); + } else { + return Err(format!( + "Merge fallback applied for {} file(s): \n{}", + merge_fallback_files.len(), + merge_fallback_files.join("\n") + ) + .into()); + } } } Ok(()) } -async fn connect( +async fn sqlx_connect( max_connections: u32, + acquire_timeout: u64, url: &str, schema: Option<&str>, ) -> Result, Box> @@ -209,7 +336,9 @@ where DB: sqlx::Database, for<'a> &'a mut ::Connection: sqlx::Executor<'a>, { - let mut pool_options = sqlx::pool::PoolOptions::::new().max_connections(max_connections); + let mut pool_options = sqlx::pool::PoolOptions::::new() + .max_connections(max_connections) + .acquire_timeout(time::Duration::from_secs(acquire_timeout)); // Set search_path for Postgres, E.g. Some("public") by default // MySQL & SQLite connection initialize with schema `None` if let Some(schema) = schema { @@ -235,9 +364,29 @@ impl From for CodegenDateTimeCrate { } } +impl From for CodegenBigIntegerType { + fn from(date_time_crate: BigIntegerType) -> CodegenBigIntegerType { + match date_time_crate { + BigIntegerType::I64 => CodegenBigIntegerType::I64, + BigIntegerType::I32 => CodegenBigIntegerType::I32, + } + } +} + +impl From for CodegenBannerVersion { + fn from(banner_version: BannerVersion) -> CodegenBannerVersion { + match banner_version { + BannerVersion::Off => CodegenBannerVersion::Off, + BannerVersion::Major => CodegenBannerVersion::Major, + BannerVersion::Minor => CodegenBannerVersion::Minor, + BannerVersion::Patch => CodegenBannerVersion::Patch, + } + } +} + #[cfg(test)] mod tests { - use clap::StructOpt; + use clap::Parser; use super::*; use crate::{Cli, Commands}; @@ -305,25 +454,6 @@ mod tests { } } - #[test] - #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: PoolTimedOut")] - fn test_generate_entity_no_password() { - let cli = Cli::parse_from([ - "sea-orm-cli", - "generate", - "entity", - "--database-url", - "mysql://root:@localhost:3306/database", - ]); - - match cli.command { - Commands::Generate { command } => { - smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); - } - _ => unreachable!(), - } - } - #[test] #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: EmptyHost")] fn test_generate_entity_no_host() { diff --git a/sea-orm-cli/src/commands/migrate.rs b/sea-orm-cli/src/commands/migrate.rs index d7eedcf553..11978b4dd8 100644 --- a/sea-orm-cli/src/commands/migrate.rs +++ b/sea-orm-cli/src/commands/migrate.rs @@ -48,6 +48,7 @@ pub fn run_migrate_command( }; // Construct the arguments that will be supplied to `cargo` command let mut args = vec!["run", "--manifest-path", &manifest_path, "--", subcommand]; + let mut envs = vec![]; let mut num: String = "".to_string(); if let Some(steps) = steps { @@ -57,17 +58,17 @@ pub fn run_migrate_command( args.extend(["-n", &num]) } if let Some(database_url) = &database_url { - args.extend(["-u", database_url]); + envs.push(("DATABASE_URL", database_url)); } if let Some(database_schema) = &database_schema { - args.extend(["-s", database_schema]); + envs.push(("DATABASE_SCHEMA", database_schema)); } if verbose { args.push("-v"); } // Run migrator CLI on user's behalf println!("Running `cargo {}`", args.join(" ")); - let exit_status = Command::new("cargo").args(args).status()?; // Get the status code + let exit_status = Command::new("cargo").args(args).envs(envs).status()?; // Get the status code if !exit_status.success() { // Propagate the error if any return Err("Fail to run migration".into()); @@ -117,7 +118,7 @@ pub fn run_migrate_init(migration_dir: &str) -> Result<(), Box> { content.replace("", &ver) }); write_file!("README.md"); - if git2::Repository::discover(Path::new(&migration_dir)).is_ok() { + if glob::glob(&format!("{migration_dir}**/.git"))?.count() > 0 { write_file!(".gitignore", "_gitignore"); } println!("Done!"); @@ -226,7 +227,11 @@ fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box< // find existing mod declarations, add new line let mod_regex = Regex::new(r"mod\s+(?Pm\d{8}_\d{6}_\w+);")?; let mods: Vec<_> = mod_regex.captures_iter(&migrator_content).collect(); - let mods_end = mods.last().unwrap().get(0).unwrap().end() + 1; + let mods_end = if let Some(last_match) = mods.last() { + last_match.get(0).unwrap().end() + 1 + } else { + migrator_content.len() + }; updated_migrator_content.insert_str(mods_end, format!("mod {migration_name};\n").as_str()); // build new vector from declared migration modules diff --git a/sea-orm-cli/template/migration/README.md b/sea-orm-cli/template/migration/README.md index b3ea53eb44..3b438d89e3 100644 --- a/sea-orm-cli/template/migration/README.md +++ b/sea-orm-cli/template/migration/README.md @@ -2,7 +2,7 @@ - Generate a new migration file ```sh - cargo run -- migrate generate MIGRATION_NAME + cargo run -- generate MIGRATION_NAME ``` - Apply all pending migrations ```sh diff --git a/sea-orm-cli/template/migration/_Cargo.toml b/sea-orm-cli/template/migration/_Cargo.toml index 2c674f238d..57797a39dc 100644 --- a/sea-orm-cli/template/migration/_Cargo.toml +++ b/sea-orm-cli/template/migration/_Cargo.toml @@ -1,22 +1,23 @@ [package] -name = "migration" -version = "0.1.0" -edition = "2021" -publish = false +edition = "2024" +name = "migration" +publish = false +rust-version = "1.85.0" +version = "0.1.0" [lib] name = "migration" path = "src/lib.rs" [dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } [dependencies.sea-orm-migration] -version = "" features = [ - # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. - # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. - # e.g. - # "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature - # "sqlx-postgres", # `DATABASE_DRIVER` feature + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + # "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature + # "sqlx-postgres", # `DATABASE_DRIVER` feature ] +version = "" diff --git a/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs b/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs index b058244632..280e98489b 100644 --- a/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs +++ b/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -12,17 +12,11 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Post::Table) + .table("post") .if_not_exists() - .col( - ColumnDef::new(Post::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Post::Title).string().not_null()) - .col(ColumnDef::new(Post::Text).string().not_null()) + .col(pk_auto("id")) + .col(string("title")) + .col(string("text")) .to_owned(), ) .await @@ -33,16 +27,7 @@ impl MigrationTrait for Migration { todo!(); manager - .drop_table(Table::drop().table(Post::Table).to_owned()) + .drop_table(Table::drop().table("post").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -enum Post { - Table, - Id, - Title, - Text, -} diff --git a/sea-orm-cli/template/migration/src/main.rs b/sea-orm-cli/template/migration/src/main.rs index c6b6e48dbc..f054deaf88 100644 --- a/sea-orm-cli/template/migration/src/main.rs +++ b/sea-orm-cli/template/migration/src/main.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -#[async_std::main] +#[tokio::main] async fn main() { cli::run_cli(migration::Migrator).await; } diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml index c3efc08da9..0a8fe95a17 100644 --- a/sea-orm-codegen/Cargo.toml +++ b/sea-orm-codegen/Cargo.toml @@ -1,29 +1,47 @@ [package] -name = "sea-orm-codegen" -version = "0.12.0" -authors = ["Billy Chan "] -edition = "2021" -description = "Code Generator for SeaORM" -license = "MIT OR Apache-2.0" -homepage = "https://www.sea-ql.org/SeaORM" +authors = ["Billy Chan "] +categories = ["database"] +description = "Code Generator for SeaORM" documentation = "https://docs.rs/sea-orm" -repository = "https://github.com/SeaQL/sea-orm" -categories = ["database"] -keywords = ["sql", "mysql", "postgres", "sqlite"] -rust-version = "1.65" +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" +keywords = ["sql", "mysql", "postgres", "sqlite"] +license = "MIT OR Apache-2.0" +name = "sea-orm-codegen" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.37" [lib] name = "sea_orm_codegen" path = "src/lib.rs" [dependencies] -sea-query = { version = "0.29.0-rc.2", default-features = false, features = ["thread-safe"] } -syn = { version = "1", default-features = false, features = ["parsing", "proc-macro", "derive", "printing"] } -quote = { version = "1", default-features = false } -heck = { version = "0.4", default-features = false } +heck = { version = "0.5", default-features = false } +pluralizer = { version = "0.5" } +prettyplease = { version = "0.2", default-features = false, features = [ + "verbatim", +] } proc-macro2 = { version = "1", default-features = false } +quote = { version = "1", default-features = false } +sea-query = { version = "1.0.0-rc.1", default-features = false, features = [ + "thread-safe", +] } +syn = { version = "2", default-features = false, features = [ + "parsing", + "proc-macro", + "derive", + "printing", + "clone-impls", + "extra-traits", + "full", + "visit", + "visit-mut", + "fold", +] } tracing = { version = "0.1", default-features = false, features = ["log"] } [dev-dependencies] +indoc = "2.0.6" pretty_assertions = { version = "0.7" } -sea-orm = { path = "../", default-features = false, features = ["macros"] } +sea-orm = { path = "../", default-features = false, features = ["macros"] } diff --git a/sea-orm-codegen/rustfmt.toml b/sea-orm-codegen/rustfmt.toml index 8b1ff05b16..7531874f50 100644 --- a/sea-orm-codegen/rustfmt.toml +++ b/sea-orm-codegen/rustfmt.toml @@ -2,4 +2,4 @@ ignore = [ "tests/compact/*.rs", "tests/expanded/*.rs", "tests_cfg", -] \ No newline at end of file +] diff --git a/sea-orm-codegen/src/entity/active_enum.rs b/sea-orm-codegen/src/entity/active_enum.rs index 30c9c5df66..86c0efe26d 100644 --- a/sea-orm-codegen/src/entity/active_enum.rs +++ b/sea-orm-codegen/src/entity/active_enum.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use sea_query::DynIden; -use crate::WithSerde; +use crate::{EntityFormat, WithSerde}; #[derive(Clone, Debug)] pub struct ActiveEnum { @@ -12,33 +12,87 @@ pub struct ActiveEnum { } impl ActiveEnum { - pub fn impl_active_enum(&self, with_serde: &WithSerde, with_copy_enums: bool) -> TokenStream { + pub fn impl_active_enum( + &self, + with_serde: &WithSerde, + with_copy_enums: bool, + extra_derives: &TokenStream, + extra_attributes: &TokenStream, + entity_format: EntityFormat, + ) -> TokenStream { let enum_name = &self.enum_name.to_string(); let enum_iden = format_ident!("{}", enum_name.to_upper_camel_case()); let values: Vec = self.values.iter().map(|v| v.to_string()).collect(); + let variants = values.iter().map(|v| v.trim()).map(|v| { - if v.chars().next().map(char::is_numeric).unwrap_or(false) { - format_ident!("_{}", v) + if v.is_empty() { + println!("Warning: item in the enumeration '{enum_name}' is an empty string, it will be converted to `__EmptyString`. You can modify it later as needed."); + return format_ident!("__EmptyString"); + } + + let is_leading_digit = v.chars().next().is_some_and(|c| c.is_ascii_digit()); + let is_valid_char = |c: char| c.is_ascii_alphanumeric() || c == '_'; + let is_passthrough = |c: char| matches!(c, '-' | ' '); + let needs_utf8_escape = |c: char| !is_valid_char(c) && !is_passthrough(c); + + if v.chars().any(needs_utf8_escape) { + println!("Warning: item '{v}' in the enumeration '{enum_name}' cannot be converted into a valid Rust enum member name. It will be converted to its corresponding UTF-8 encoding. You can modify it later as needed."); + + let mut buf = String::new(); + + if is_leading_digit { + buf.push('_'); + } + + for c in v.chars() { + if is_passthrough(c) { + continue; + } else if is_valid_char(c) || c.len_utf8() > 1 { + buf.push(c); + } else { + buf.push_str(&format!("U{:04X}", c as u32)); + } + } + + return format_ident!("{buf}"); + } + + if is_leading_digit { + let sanitized = v.chars().filter(|&c| is_valid_char(c)).collect::(); + format_ident!("_{sanitized}") } else { format_ident!("{}", v.to_upper_camel_case()) } }); - let extra_derive = with_serde.extra_derive(); + let serde_derive = with_serde.extra_derive(); let copy_derive = if with_copy_enums { quote! { , Copy } } else { quote! {} }; - quote! { - #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum #copy_derive #extra_derive)] - #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = #enum_name)] - pub enum #enum_iden { - #( - #[sea_orm(string_value = #values)] - #variants, - )* + if entity_format == EntityFormat::Frontend { + quote! { + #[derive(Debug, Clone, PartialEq, Eq #copy_derive #serde_derive #extra_derives)] + #extra_attributes + pub enum #enum_iden { + #( + #variants, + )* + } + } + } else { + quote! { + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum #copy_derive #serde_derive #extra_derives)] + #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = #enum_name)] + #extra_attributes + pub enum #enum_iden { + #( + #[sea_orm(string_value = #values)] + #variants, + )* + } } } } @@ -47,9 +101,40 @@ impl ActiveEnum { #[cfg(test)] mod tests { use super::*; + use crate::entity::writer::{bonus_attributes, bonus_derive}; use pretty_assertions::assert_eq; use sea_query::{Alias, IntoIden}; + #[test] + fn test_enum_variant_starts_with_empty_string() { + assert_eq!( + ActiveEnum { + enum_name: Alias::new("media_type").into_iden(), + values: vec![""] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + } + .impl_active_enum( + &WithSerde::None, + true, + &TokenStream::new(), + &TokenStream::new(), + EntityFormat::Compact, + ) + .to_string(), + quote!( + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] + #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "media_type")] + pub enum MediaType { + #[sea_orm(string_value = "")] + __EmptyString, + } + ) + .to_string() + ) + } + #[test] fn test_enum_variant_starts_with_number() { assert_eq!( @@ -67,12 +152,19 @@ mod tests { "EXECUTABLE", "ARCHIVE", "3D", + "2-D" ] .into_iter() .map(|variant| Alias::new(variant).into_iden()) .collect(), } - .impl_active_enum(&WithSerde::None, true) + .impl_active_enum( + &WithSerde::None, + true, + &TokenStream::new(), + &TokenStream::new(), + EntityFormat::Compact, + ) .to_string(), quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] @@ -100,6 +192,181 @@ mod tests { Archive, #[sea_orm(string_value = "3D")] _3D, + #[sea_orm(string_value = "2-D")] + _2D, + } + ) + .to_string() + ) + } + + #[test] + fn test_enum_extra_derives() { + assert_eq!( + ActiveEnum { + enum_name: Alias::new("media_type").into_iden(), + values: vec!["UNKNOWN", "BITMAP",] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + } + .impl_active_enum( + &WithSerde::None, + true, + &bonus_derive(["specta::Type", "ts_rs::TS"]), + &TokenStream::new(), + EntityFormat::Compact, + ) + .to_string(), + build_generated_enum(), + ); + + #[rustfmt::skip] + fn build_generated_enum() -> String { + quote!( + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, specta :: Type, ts_rs :: TS)] + #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "media_type")] + pub enum MediaType { + #[sea_orm(string_value = "UNKNOWN")] + Unknown, + #[sea_orm(string_value = "BITMAP")] + Bitmap, + } + ) + .to_string() + } + } + + #[test] + fn test_enum_extra_attributes() { + assert_eq!( + ActiveEnum { + enum_name: Alias::new("coinflip_result_type").into_iden(), + values: vec!["HEADS", "TAILS"] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + } + .impl_active_enum( + &WithSerde::None, + true, + &TokenStream::new(), + &bonus_attributes([r#"serde(rename_all = "camelCase")"#]), + EntityFormat::Compact, + ) + .to_string(), + quote!( + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] + #[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "coinflip_result_type" + )] + #[serde(rename_all = "camelCase")] + pub enum CoinflipResultType { + #[sea_orm(string_value = "HEADS")] + Heads, + #[sea_orm(string_value = "TAILS")] + Tails, + } + ) + .to_string() + ); + assert_eq!( + ActiveEnum { + enum_name: Alias::new("coinflip_result_type").into_iden(), + values: vec!["HEADS", "TAILS"] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + } + .impl_active_enum( + &WithSerde::None, + true, + &TokenStream::new(), + &bonus_attributes([r#"serde(rename_all = "camelCase")"#, "ts(export)"]), + EntityFormat::Compact, + ) + .to_string(), + quote!( + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] + #[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "coinflip_result_type" + )] + #[serde(rename_all = "camelCase")] + #[ts(export)] + pub enum CoinflipResultType { + #[sea_orm(string_value = "HEADS")] + Heads, + #[sea_orm(string_value = "TAILS")] + Tails, + } + ) + .to_string() + ) + } + + #[test] + fn test_enum_variant_utf8_encode() { + assert_eq!( + ActiveEnum { + enum_name: Alias::new("ty").into_iden(), + values: vec![ + "Question", + "QuestionsAdditional", + "Answer", + "Other", + "/", + "//", + "A-B-C", + "你好", + "0/", + "0//", + "0A-B-C", + "0你好", + ] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + } + .impl_active_enum( + &WithSerde::None, + true, + &TokenStream::new(), + &TokenStream::new(), + EntityFormat::Compact, + ) + .to_string(), + quote!( + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] + #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "ty")] + pub enum Ty { + #[sea_orm(string_value = "Question")] + Question, + #[sea_orm(string_value = "QuestionsAdditional")] + QuestionsAdditional, + #[sea_orm(string_value = "Answer")] + Answer, + #[sea_orm(string_value = "Other")] + Other, + #[sea_orm(string_value = "/")] + U002F, + #[sea_orm(string_value = "//")] + U002FU002F, + #[sea_orm(string_value = "A-B-C")] + ABC, + #[sea_orm(string_value = "你好")] + 你好, + #[sea_orm(string_value = "0/")] + _0U002F, + #[sea_orm(string_value = "0//")] + _0U002FU002F, + #[sea_orm(string_value = "0A-B-C")] + _0ABC, + #[sea_orm(string_value = "0你好")] + _0你好, } ) .to_string() diff --git a/sea-orm-codegen/src/entity/base_entity.rs b/sea-orm-codegen/src/entity/base_entity.rs index c7a89b96f3..865130dc68 100644 --- a/sea-orm-codegen/src/entity/base_entity.rs +++ b/sea-orm-codegen/src/entity/base_entity.rs @@ -5,7 +5,7 @@ use quote::quote; use sea_query::ColumnType; use crate::{ - util::escape_rust_keyword, Column, ConjunctRelation, DateTimeCrate, PrimaryKey, Relation, + Column, ColumnOption, ConjunctRelation, PrimaryKey, Relation, util::escape_rust_keyword, }; #[derive(Clone, Debug)] @@ -48,11 +48,11 @@ impl Entity { .collect() } - pub fn get_column_rs_types(&self, date_time_crate: &DateTimeCrate) -> Vec { + pub fn get_column_rs_types(&self, opt: &ColumnOption) -> Vec { self.columns .clone() .into_iter() - .map(|col| col.get_rs_type(date_time_crate)) + .map(|col| col.get_rs_type(opt)) .collect() } @@ -108,7 +108,7 @@ impl Entity { self.get_relation_enum_name() .into_iter() .chain(self_relations_reverse) - .chain(conjunct_related_names.into_iter()) + .chain(conjunct_related_names) .collect() } @@ -120,6 +120,36 @@ impl Entity { self.relations.iter().map(|rel| rel.get_attrs()).collect() } + /// Trimmed get_related_entity_attrs down to just the entity module + pub fn get_related_entity_modules(&self) -> Vec { + // 1st step get conjunct relations data + let conjunct_related_attrs = self + .conjunct_relations + .iter() + .map(|conj| conj.get_to_snake_case()); + + // helper function that generates attributes for `Relation` data + let produce_relation_attrs = |rel: &Relation, _reverse: bool| match rel.get_module_name() { + Some(module_name) => module_name, + None => format_ident!("self"), + }; + + // 2nd step get reverse self relations data + let self_relations_reverse_attrs = self + .relations + .iter() + .filter(|rel| rel.self_referencing) + .map(|rel| produce_relation_attrs(rel, true)); + + // 3rd step get normal relations data + self.relations + .iter() + .map(|rel| produce_relation_attrs(rel, false)) + .chain(self_relations_reverse_attrs) + .chain(conjunct_related_attrs) + .collect() + } + /// Used to generate the attributes for the `enum RelatedEntity` that is useful to the Seaography project pub fn get_related_entity_attrs(&self) -> Vec { // 1st step get conjunct relations data @@ -136,7 +166,7 @@ impl Entity { // helper function that generates attributes for `Relation` data let produce_relation_attrs = |rel: &Relation, reverse: bool| { let entity = match rel.get_module_name() { - Some(module_name) => format!("super::{}::Entity", module_name), + Some(module_name) => format!("super::{module_name}::Entity"), None => String::from("Entity"), }; @@ -183,7 +213,7 @@ impl Entity { format_ident!("{}", auto_increment) } - pub fn get_primary_key_rs_type(&self, date_time_crate: &DateTimeCrate) -> TokenStream { + pub fn get_primary_key_rs_type(&self, opt: &ColumnOption) -> TokenStream { let types = self .primary_keys .iter() @@ -192,7 +222,7 @@ impl Entity { .iter() .find(|col| col.name.eq(&primary_key.name)) .unwrap() - .get_rs_type(date_time_crate) + .get_rs_type(opt) .to_string() }) .collect::>(); @@ -234,6 +264,7 @@ impl Entity { match col_type { ColumnType::Float | ColumnType::Double => true, ColumnType::Array(col_type) => is_floats(col_type), + ColumnType::Vector(_) => true, _ => false, } } @@ -267,9 +298,9 @@ impl Entity { #[cfg(test)] mod tests { use quote::{format_ident, quote}; - use sea_query::{ColumnType, ForeignKeyAction}; + use sea_query::{ColumnType, ForeignKeyAction, StringLen}; - use crate::{Column, DateTimeCrate, Entity, PrimaryKey, Relation, RelationType}; + use crate::{Column, ColumnOption, Entity, PrimaryKey, Relation, RelationType}; fn setup() -> Entity { Entity { @@ -281,13 +312,15 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), - col_type: ColumnType::String(None), + col_type: ColumnType::String(StringLen::None), auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![ @@ -379,17 +412,12 @@ mod tests { #[test] fn test_get_column_rs_types() { let entity = setup(); + let opt = ColumnOption::default(); - for (i, elem) in entity - .get_column_rs_types(&DateTimeCrate::Chrono) - .into_iter() - .enumerate() - { + for (i, elem) in entity.get_column_rs_types(&opt).into_iter().enumerate() { assert_eq!( elem.to_string(), - entity.columns[i] - .get_rs_type(&DateTimeCrate::Chrono) - .to_string() + entity.columns[i].get_rs_type(&opt).to_string() ); } } @@ -487,14 +515,11 @@ mod tests { #[test] fn test_get_primary_key_rs_type() { let entity = setup(); + let opt = Default::default(); assert_eq!( - entity - .get_primary_key_rs_type(&DateTimeCrate::Chrono) - .to_string(), - entity.columns[0] - .get_rs_type(&DateTimeCrate::Chrono) - .to_string() + entity.get_primary_key_rs_type(&opt).to_string(), + entity.columns[0].get_rs_type(&opt).to_string() ); } diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index 50582c27f8..25f650fee7 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -1,17 +1,24 @@ -use crate::{util::escape_rust_keyword, DateTimeCrate}; +use crate::{BigIntegerType, DateTimeCrate, util::escape_rust_keyword}; use heck::{ToSnakeCase, ToUpperCamelCase}; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; -use sea_query::{BlobSize, ColumnDef, ColumnSpec, ColumnType}; +use sea_query::{ColumnDef, ColumnType, StringLen}; use std::fmt::Write as FmtWrite; -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct Column { pub(crate) name: String, pub(crate) col_type: ColumnType, pub(crate) auto_increment: bool, pub(crate) not_null: bool, pub(crate) unique: bool, + pub(crate) unique_key: Option, +} + +#[derive(Debug, Default, Copy, Clone)] +pub struct ColumnOption { + pub(crate) date_time_crate: DateTimeCrate, + pub(crate) big_integer_type: BigIntegerType, } impl Column { @@ -27,8 +34,8 @@ impl Column { self.name.to_snake_case() == self.name } - pub fn get_rs_type(&self, date_time_crate: &DateTimeCrate) -> TokenStream { - fn write_rs_type(col_type: &ColumnType, date_time_crate: &DateTimeCrate) -> String { + pub fn get_rs_type(&self, opt: &ColumnOption) -> TokenStream { + fn write_rs_type(col_type: &ColumnType, opt: &ColumnOption) -> String { #[allow(unreachable_patterns)] match col_type { ColumnType::Char(_) @@ -38,7 +45,11 @@ impl Column { ColumnType::TinyInteger => "i8".to_owned(), ColumnType::SmallInteger => "i16".to_owned(), ColumnType::Integer => "i32".to_owned(), - ColumnType::BigInteger => "i64".to_owned(), + ColumnType::BigInteger => match opt.big_integer_type { + BigIntegerType::I64 => "i64", + BigIntegerType::I32 => "i32", + } + .to_owned(), ColumnType::TinyUnsigned => "u8".to_owned(), ColumnType::SmallUnsigned => "u16".to_owned(), ColumnType::Unsigned => "u32".to_owned(), @@ -46,41 +57,48 @@ impl Column { ColumnType::Float => "f32".to_owned(), ColumnType::Double => "f64".to_owned(), ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(), - ColumnType::Date => match date_time_crate { + ColumnType::Date => match opt.date_time_crate { DateTimeCrate::Chrono => "Date".to_owned(), DateTimeCrate::Time => "TimeDate".to_owned(), }, - ColumnType::Time => match date_time_crate { + ColumnType::Time => match opt.date_time_crate { DateTimeCrate::Chrono => "Time".to_owned(), DateTimeCrate::Time => "TimeTime".to_owned(), }, - ColumnType::DateTime => match date_time_crate { + ColumnType::DateTime => match opt.date_time_crate { DateTimeCrate::Chrono => "DateTime".to_owned(), DateTimeCrate::Time => "TimeDateTime".to_owned(), }, - ColumnType::Timestamp => match date_time_crate { + ColumnType::Timestamp => match opt.date_time_crate { DateTimeCrate::Chrono => "DateTimeUtc".to_owned(), - // ColumnType::Timpestamp(_) => time::PrimitiveDateTime: https://docs.rs/sqlx/0.3.5/sqlx/postgres/types/index.html#time DateTimeCrate::Time => "TimeDateTime".to_owned(), }, - ColumnType::TimestampWithTimeZone => match date_time_crate { + ColumnType::TimestampWithTimeZone => match opt.date_time_crate { DateTimeCrate::Chrono => "DateTimeWithTimeZone".to_owned(), DateTimeCrate::Time => "TimeDateTimeWithTimeZone".to_owned(), }, ColumnType::Decimal(_) | ColumnType::Money(_) => "Decimal".to_owned(), ColumnType::Uuid => "Uuid".to_owned(), - ColumnType::Binary(_) | ColumnType::VarBinary(_) => "Vec".to_owned(), + ColumnType::Binary(_) | ColumnType::VarBinary(_) | ColumnType::Blob => { + "Vec".to_owned() + } ColumnType::Boolean => "bool".to_owned(), ColumnType::Enum { name, .. } => name.to_string().to_upper_camel_case(), ColumnType::Array(column_type) => { - format!("Vec<{}>", write_rs_type(column_type, date_time_crate)) + format!("Vec<{}>", write_rs_type(column_type, opt)) + } + ColumnType::Vector(_) => "PgVector".to_owned(), + ColumnType::Bit(None | Some(1)) => "bool".to_owned(), + ColumnType::Bit(_) | ColumnType::VarBit(_) => "Vec".to_owned(), + ColumnType::Year => "i32".to_owned(), + ColumnType::Cidr | ColumnType::Inet => "IpNetwork".to_owned(), + ColumnType::Interval(_, _) | ColumnType::MacAddr | ColumnType::LTree => { + "String".to_owned() } _ => unimplemented!(), } } - let ident: TokenStream = write_rs_type(&self.col_type, date_time_crate) - .parse() - .unwrap(); + let ident: TokenStream = write_rs_type(&self.col_type, opt).parse().unwrap(); match self.not_null { true => quote! { #ident }, false => quote! { Option<#ident> }, @@ -95,15 +113,18 @@ impl Column { ColumnType::Money(Some((p, s))) => Some(format!("Money(Some({p}, {s}))")), ColumnType::Text => Some("Text".to_owned()), ColumnType::JsonBinary => Some("JsonBinary".to_owned()), - ColumnType::Custom(iden) => Some(format!("custom(\"{}\")", iden.to_string())), - ColumnType::Binary(BlobSize::Blob(None)) => Some("Binary(BlobSize::Blob(None))".into()), - ColumnType::Binary(BlobSize::Blob(Some(s))) => { - Some(format!("Binary(BlobSize::Blob(Some({s})))")) + ColumnType::Custom(iden) => { + let ty = format!("custom(\"{iden}\")"); + return Some(quote! ( ignore, column_type = #ty, select_as = "text" )); } - ColumnType::Binary(BlobSize::Tiny) => Some("Binary(BlobSize::Tiny)".into()), - ColumnType::Binary(BlobSize::Medium) => Some("Binary(BlobSize::Medium)".into()), - ColumnType::Binary(BlobSize::Long) => Some("Binary(BlobSize::Long)".into()), - ColumnType::VarBinary(s) => Some(format!("VarBinary({s})")), + ColumnType::Binary(s) => Some(format!("Binary({s})")), + ColumnType::VarBinary(s) => match s { + StringLen::N(s) => Some(format!("VarBinary(StringLen::N({s}))")), + StringLen::None => Some("VarBinary(StringLen::None)".to_owned()), + StringLen::Max => Some("VarBinary(StringLen::Max)".to_owned()), + }, + ColumnType::Blob => Some("Blob".to_owned()), + ColumnType::Cidr => Some("Cidr".to_owned()), _ => None, }; col_type.map(|ty| quote! { column_type = #ty }) @@ -117,8 +138,9 @@ impl Column { None => quote! { ColumnType::Char(None) }, }, ColumnType::String(s) => match s { - Some(s) => quote! { ColumnType::String(Some(#s)) }, - None => quote! { ColumnType::String(None) }, + StringLen::N(s) => quote! { ColumnType::String(StringLen::N(#s)) }, + StringLen::None => quote! { ColumnType::String(StringLen::None) }, + StringLen::Max => quote! { ColumnType::String(StringLen::Max) }, }, ColumnType::Text => quote! { ColumnType::Text }, ColumnType::TinyInteger => quote! { ColumnType::TinyInteger }, @@ -142,24 +164,15 @@ impl Column { } ColumnType::Time => quote! { ColumnType::Time }, ColumnType::Date => quote! { ColumnType::Date }, - ColumnType::Binary(BlobSize::Blob(None)) => { - quote! { ColumnType::Binary(BlobSize::Blob(None)) } - } - ColumnType::Binary(BlobSize::Blob(Some(s))) => { - quote! { ColumnType::Binary(BlobSize::Blob(Some(#s))) } - } - ColumnType::Binary(BlobSize::Tiny) => { - quote! { ColumnType::Binary(BlobSize::Tiny) } - } - ColumnType::Binary(BlobSize::Medium) => { - quote! { ColumnType::Binary(BlobSize::Medium) } - } - ColumnType::Binary(BlobSize::Long) => { - quote! { ColumnType::Binary(BlobSize::Long) } - } - ColumnType::VarBinary(s) => { - quote! { ColumnType::VarBinary(#s) } + ColumnType::Binary(s) => { + quote! { ColumnType::Binary(#s) } } + ColumnType::VarBinary(s) => match s { + StringLen::N(s) => quote! { ColumnType::VarBinary(StringLen::N(#s)) }, + StringLen::None => quote! { ColumnType::VarBinary(StringLen::None) }, + StringLen::Max => quote! { ColumnType::VarBinary(StringLen::Max) }, + }, + ColumnType::Blob => quote! { ColumnType::Blob }, ColumnType::Boolean => quote! { ColumnType::Boolean }, ColumnType::Money(s) => match s { Some((s1, s2)) => quote! { ColumnType::Money(Some((#s1, #s2))) }, @@ -168,18 +181,28 @@ impl Column { ColumnType::Json => quote! { ColumnType::Json }, ColumnType::JsonBinary => quote! { ColumnType::JsonBinary }, ColumnType::Uuid => quote! { ColumnType::Uuid }, + ColumnType::Cidr => quote! { ColumnType::Cidr }, + ColumnType::Inet => quote! { ColumnType::Inet }, ColumnType::Custom(s) => { let s = s.to_string(); quote! { ColumnType::custom(#s) } } ColumnType::Enum { name, .. } => { let enum_ident = format_ident!("{}", name.to_string().to_upper_camel_case()); - quote! { #enum_ident::db_type() } + quote! { + #enum_ident::db_type() + .get_column_type() + .to_owned() + } } ColumnType::Array(column_type) => { let column_type = write_col_def(column_type); quote! { ColumnType::Array(RcOrArc::new(#column_type)) } } + ColumnType::Vector(size) => match size { + Some(size) => quote! { ColumnType::Vector(Some(#size)) }, + None => quote! { ColumnType::Vector(None) }, + }, #[allow(unreachable_patterns)] _ => unimplemented!(), } @@ -201,12 +224,9 @@ impl Column { col_def } - pub fn get_info(&self, date_time_crate: &DateTimeCrate) -> String { + pub fn get_info(&self, opt: &ColumnOption) -> String { let mut info = String::new(); - let type_info = self - .get_rs_type(date_time_crate) - .to_string() - .replace(' ', ""); + let type_info = self.get_rs_type(opt).to_string().replace(' ', ""); let col_info = self.col_info(); write!( &mut info, @@ -249,6 +269,13 @@ impl Column { quote! {} } } + + pub fn get_inner_col_type(&self) -> &ColumnType { + match &self.col_type { + ColumnType::Array(inner_col_type) => inner_col_type.as_ref(), + _ => &self.col_type, + } + } } impl From for Column { @@ -264,34 +291,43 @@ impl From<&ColumnDef> for Column { Some(ty) => ty.clone(), None => panic!("ColumnType should not be empty"), }; - let auto_increment = col_def - .get_column_spec() - .iter() - .any(|spec| matches!(spec, ColumnSpec::AutoIncrement)); - let not_null = col_def - .get_column_spec() - .iter() - .any(|spec| matches!(spec, ColumnSpec::NotNull)); - let unique = col_def - .get_column_spec() - .iter() - .any(|spec| matches!(spec, ColumnSpec::UniqueKey)); + let auto_increment = col_def.get_column_spec().auto_increment; + let not_null = match col_def.get_column_spec().nullable { + Some(nullable) => !nullable, + None => false, + }; + let unique = col_def.get_column_spec().unique; Self { name, col_type, auto_increment, not_null, unique, + unique_key: None, } } } #[cfg(test)] mod tests { - use crate::{Column, DateTimeCrate}; + use crate::{Column, ColumnOption, DateTimeCrate}; use proc_macro2::TokenStream; use quote::quote; - use sea_query::{Alias, BlobSize, ColumnDef, ColumnType, SeaRc}; + use sea_query::{Alias, ColumnDef, ColumnType, SeaRc, StringLen}; + + fn date_time_crate_chrono() -> ColumnOption { + ColumnOption { + date_time_crate: DateTimeCrate::Chrono, + big_integer_type: Default::default(), + } + } + + fn date_time_crate_time() -> ColumnOption { + ColumnOption { + date_time_crate: DateTimeCrate::Time, + big_integer_type: Default::default(), + } + } fn setup() -> Vec { macro_rules! make_col { @@ -302,11 +338,13 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, } }; } vec![ - make_col!("id", ColumnType::String(Some(255))), + make_col!("id", ColumnType::String(StringLen::N(255))), + make_col!("id", ColumnType::String(StringLen::None)), make_col!( "cake_id", ColumnType::Custom(SeaRc::new(Alias::new("cus_col"))) @@ -321,15 +359,10 @@ mod tests { make_col!("CakeFillingId", ColumnType::BigUnsigned), make_col!("cake-filling-id", ColumnType::Float), make_col!("CAKE_FILLING_ID", ColumnType::Double), - make_col!("CAKE-FILLING-ID", ColumnType::Binary(BlobSize::Blob(None))), - make_col!( - "CAKE-FILLING-ID", - ColumnType::Binary(BlobSize::Blob(Some(10))) - ), - make_col!("CAKE-FILLING-ID", ColumnType::Binary(BlobSize::Tiny)), - make_col!("CAKE-FILLING-ID", ColumnType::Binary(BlobSize::Medium)), - make_col!("CAKE-FILLING-ID", ColumnType::Binary(BlobSize::Long)), - make_col!("CAKE-FILLING-ID", ColumnType::VarBinary(10)), + make_col!("CAKE-FILLING-ID", ColumnType::Binary(10)), + make_col!("CAKE-FILLING-ID", ColumnType::VarBinary(StringLen::None)), + make_col!("CAKE-FILLING-ID", ColumnType::VarBinary(StringLen::N(10))), + make_col!("CAKE-FILLING-ID", ColumnType::VarBinary(StringLen::Max)), make_col!("CAKE", ColumnType::Boolean), make_col!("date", ColumnType::Date), make_col!("time", ColumnType::Time), @@ -343,6 +376,7 @@ mod tests { fn test_get_name_snake_case() { let columns = setup(); let snack_cases = vec![ + "id", "id", "cake_id", "cake_id", @@ -359,8 +393,6 @@ mod tests { "cake_filling_id", "cake_filling_id", "cake_filling_id", - "cake_filling_id", - "cake_filling_id", "cake", "date", "time", @@ -377,6 +409,7 @@ mod tests { fn test_get_name_camel_case() { let columns = setup(); let camel_cases = vec![ + "Id", "Id", "CakeId", "CakeId", @@ -393,8 +426,6 @@ mod tests { "CakeFillingId", "CakeFillingId", "CakeFillingId", - "CakeFillingId", - "CakeFillingId", "Cake", "Date", "Time", @@ -410,8 +441,8 @@ mod tests { #[test] fn test_get_rs_type_with_chrono() { let columns = setup(); - let chrono_crate = DateTimeCrate::Chrono; let rs_types = vec![ + "String", "String", "String", "i8", @@ -428,8 +459,6 @@ mod tests { "Vec", "Vec", "Vec", - "Vec", - "Vec", "bool", "Date", "Time", @@ -442,13 +471,13 @@ mod tests { col.not_null = true; assert_eq!( - col.get_rs_type(&chrono_crate).to_string(), + col.get_rs_type(&date_time_crate_chrono()).to_string(), quote!(#rs_type).to_string() ); col.not_null = false; assert_eq!( - col.get_rs_type(&chrono_crate).to_string(), + col.get_rs_type(&date_time_crate_chrono()).to_string(), quote!(Option<#rs_type>).to_string() ); } @@ -457,8 +486,8 @@ mod tests { #[test] fn test_get_rs_type_with_time() { let columns = setup(); - let time_crate = DateTimeCrate::Time; let rs_types = vec![ + "String", "String", "String", "i8", @@ -475,8 +504,6 @@ mod tests { "Vec", "Vec", "Vec", - "Vec", - "Vec", "bool", "TimeDate", "TimeTime", @@ -489,13 +516,13 @@ mod tests { col.not_null = true; assert_eq!( - col.get_rs_type(&time_crate).to_string(), + col.get_rs_type(&date_time_crate_time()).to_string(), quote!(#rs_type).to_string() ); col.not_null = false; assert_eq!( - col.get_rs_type(&time_crate).to_string(), + col.get_rs_type(&date_time_crate_time()).to_string(), quote!(Option<#rs_type>).to_string() ); } @@ -505,7 +532,8 @@ mod tests { fn test_get_def() { let columns = setup(); let col_defs = vec![ - "ColumnType::String(Some(255u32)).def()", + "ColumnType::String(StringLen::N(255u32)).def()", + "ColumnType::String(StringLen::None).def()", "ColumnType::custom(\"cus_col\").def()", "ColumnType::TinyInteger.def()", "ColumnType::TinyUnsigned.def()", @@ -517,12 +545,10 @@ mod tests { "ColumnType::BigUnsigned.def()", "ColumnType::Float.def()", "ColumnType::Double.def()", - "ColumnType::Binary(BlobSize::Blob(None)).def()", - "ColumnType::Binary(BlobSize::Blob(Some(10u32))).def()", - "ColumnType::Binary(BlobSize::Tiny).def()", - "ColumnType::Binary(BlobSize::Medium).def()", - "ColumnType::Binary(BlobSize::Long).def()", - "ColumnType::VarBinary(10u32).def()", + "ColumnType::Binary(10u32).def()", + "ColumnType::VarBinary(StringLen::None).def()", + "ColumnType::VarBinary(StringLen::N(10u32)).def()", + "ColumnType::VarBinary(StringLen::Max).def()", "ColumnType::Boolean.def()", "ColumnType::Date.def()", "ColumnType::Time.def()", @@ -550,7 +576,7 @@ mod tests { fn test_get_info() { let column: Column = ColumnDef::new(Alias::new("id")).string().to_owned().into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `id`: Option" ); @@ -560,7 +586,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `id`: String, not_null" ); @@ -571,7 +597,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `id`: String, not_null, unique" ); @@ -583,7 +609,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `id`: String, auto_increment, not_null, unique" ); @@ -593,7 +619,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `date_field`: Date, not_null" ); @@ -603,7 +629,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Time).as_str(), + column.get_info(&date_time_crate_time()).as_str(), "Column `date_field`: TimeDate, not_null" ); @@ -613,7 +639,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `time_field`: Time, not_null" ); @@ -623,7 +649,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Time).as_str(), + column.get_info(&date_time_crate_time()).as_str(), "Column `time_field`: TimeTime, not_null" ); @@ -633,7 +659,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `date_time_field`: DateTime, not_null" ); @@ -643,7 +669,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Time).as_str(), + column.get_info(&date_time_crate_time()).as_str(), "Column `date_time_field`: TimeDateTime, not_null" ); @@ -653,7 +679,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `timestamp_field`: DateTimeUtc, not_null" ); @@ -663,7 +689,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Time).as_str(), + column.get_info(&date_time_crate_time()).as_str(), "Column `timestamp_field`: TimeDateTime, not_null" ); @@ -673,7 +699,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Chrono).as_str(), + column.get_info(&date_time_crate_chrono()).as_str(), "Column `timestamp_with_timezone_field`: DateTimeWithTimeZone, not_null" ); @@ -683,7 +709,7 @@ mod tests { .to_owned() .into(); assert_eq!( - column.get_info(&DateTimeCrate::Time).as_str(), + column.get_info(&date_time_crate_time()).as_str(), "Column `timestamp_with_timezone_field`: TimeDateTimeWithTimeZone, not_null" ); } @@ -694,7 +720,7 @@ mod tests { assert_eq!( column.get_def().to_string(), quote! { - ColumnType::String(None).def().null() + ColumnType::String(StringLen::None).def().null() } .to_string() ); diff --git a/sea-orm-codegen/src/entity/relation.rs b/sea-orm-codegen/src/entity/relation.rs index 67fa77ad53..88a198e418 100644 --- a/sea-orm-codegen/src/entity/relation.rs +++ b/sea-orm-codegen/src/entity/relation.rs @@ -1,12 +1,12 @@ -use crate::util::unpack_table_ref; use heck::{ToSnakeCase, ToUpperCamelCase}; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use sea_query::{ForeignKeyAction, TableForeignKey}; +use syn::{punctuated::Punctuated, token::Comma}; use crate::util::escape_rust_keyword; -#[derive(Clone, Debug)] +#[derive(Debug, Clone, Copy)] pub enum RelationType { HasOne, HasMany, @@ -66,17 +66,27 @@ impl Relation { } } RelationType::BelongsTo => { - let column_camel_case = self.get_column_camel_case(); - let ref_column_camel_case = self.get_ref_column_camel_case(); - let to_col = if module_name.is_some() { - quote! { super::#module_name::Column::#ref_column_camel_case } - } else { - quote! { Column::#ref_column_camel_case } + let map_src_column = |src_column: &Ident| { + quote! { Column::#src_column } + }; + let map_ref_column = |ref_column: &Ident| { + if module_name.is_some() { + quote! { super::#module_name::Column::#ref_column } + } else { + quote! { Column::#ref_column } + } }; + let map_punctuated = + |punctuated: Punctuated| match punctuated.len() { + 0..=1 => quote! { #punctuated }, + _ => quote! { (#punctuated) }, + }; + let (from, to) = + self.get_src_ref_columns(map_src_column, map_ref_column, map_punctuated); quote! { Entity::#rel_type(#ref_entity) - .from(Column::#column_camel_case) - .to(#to_col) + .from(#from) + .to(#to) .into() } } @@ -98,10 +108,20 @@ impl Relation { } } RelationType::BelongsTo => { - let column_camel_case = self.get_column_camel_case(); - let ref_column_camel_case = self.get_ref_column_camel_case(); - let from = format!("Column::{column_camel_case}"); - let to = format!("{module_name}Column::{ref_column_camel_case}"); + let map_src_column = |src_column: &Ident| format!("Column::{src_column}"); + let map_ref_column = + |ref_column: &Ident| format!("{module_name}Column::{ref_column}"); + let map_punctuated = |punctuated: Vec| { + let len = punctuated.len(); + let punctuated = punctuated.join(", "); + match len { + 0..=1 => punctuated, + _ => format!("({punctuated})"), + } + }; + let (from, to) = + self.get_src_ref_columns(map_src_column, map_ref_column, map_punctuated); + let on_update = if let Some(action) = &self.on_update { let action = Self::get_foreign_key_action(action); quote! { @@ -139,30 +159,59 @@ impl Relation { } } - pub fn get_column_camel_case(&self) -> Ident { - format_ident!("{}", self.columns[0].to_upper_camel_case()) + pub fn get_column_camel_case(&self) -> Vec { + self.columns + .iter() + .map(|col| format_ident!("{}", col.to_upper_camel_case())) + .collect() } - pub fn get_ref_column_camel_case(&self) -> Ident { - format_ident!("{}", self.ref_columns[0].to_upper_camel_case()) + pub fn get_ref_column_camel_case(&self) -> Vec { + self.ref_columns + .iter() + .map(|col| format_ident!("{}", col.to_upper_camel_case())) + .collect() } pub fn get_foreign_key_action(action: &ForeignKeyAction) -> String { - match action { - ForeignKeyAction::Restrict => "Restrict", - ForeignKeyAction::Cascade => "Cascade", - ForeignKeyAction::SetNull => "SetNull", - ForeignKeyAction::NoAction => "NoAction", - ForeignKeyAction::SetDefault => "SetDefault", - } - .to_owned() + action.variant_name().to_owned() + } + + pub fn get_src_ref_columns( + &self, + map_src_column: F1, + map_ref_column: F2, + map_punctuated: F3, + ) -> (T, T) + where + F1: Fn(&Ident) -> T, + F2: Fn(&Ident) -> T, + F3: Fn(I) -> T, + I: Extend + Default, + { + let from: I = + self.get_column_camel_case() + .iter() + .fold(I::default(), |mut acc, src_column| { + acc.extend([map_src_column(src_column)]); + acc + }); + let to: I = + self.get_ref_column_camel_case() + .iter() + .fold(I::default(), |mut acc, ref_column| { + acc.extend([map_ref_column(ref_column)]); + acc + }); + + (map_punctuated(from), map_punctuated(to)) } } impl From<&TableForeignKey> for Relation { fn from(tbl_fk: &TableForeignKey) -> Self { let ref_table = match tbl_fk.get_ref_table() { - Some(s) => unpack_table_ref(s), + Some(s) => s.sea_orm_table().to_string(), None => panic!("RefTable should not be empty"), }; let columns = tbl_fk.get_columns(); @@ -278,7 +327,7 @@ mod tests { let relations = setup(); let cols = vec!["Id", "FillingId", "FillingId"]; for (rel, col) in relations.into_iter().zip(cols) { - assert_eq!(rel.get_column_camel_case(), col); + assert_eq!(rel.get_column_camel_case(), [col]); } } @@ -287,7 +336,7 @@ mod tests { let relations = setup(); let ref_cols = vec!["CakeId", "Id", "Id"]; for (rel, ref_col) in relations.into_iter().zip(ref_cols) { - assert_eq!(rel.get_ref_column_camel_case(), ref_col); + assert_eq!(rel.get_ref_column_camel_case(), [ref_col]); } } } diff --git a/sea-orm-codegen/src/entity/transformer.rs b/sea-orm-codegen/src/entity/transformer.rs index 421a83bf18..fcb782bdaf 100644 --- a/sea-orm-codegen/src/entity/transformer.rs +++ b/sea-orm-codegen/src/entity/transformer.rs @@ -1,9 +1,9 @@ use crate::{ - util::unpack_table_ref, ActiveEnum, Column, ConjunctRelation, Entity, EntityWriter, Error, - PrimaryKey, Relation, RelationType, + ActiveEnum, Column, ConjunctRelation, Entity, EntityWriter, Error, PrimaryKey, Relation, + RelationType, }; -use sea_query::{ColumnSpec, TableCreateStatement}; -use std::collections::{BTreeMap, HashMap}; +use sea_query::TableCreateStatement; +use std::collections::{BTreeMap, HashMap, HashSet}; #[derive(Clone, Debug)] pub struct EntityTransformer; @@ -15,30 +15,19 @@ impl EntityTransformer { let mut entities = BTreeMap::new(); for table_create in table_create_stmts.into_iter() { let table_name = match table_create.get_table_name() { - Some(table_ref) => match table_ref { - sea_query::TableRef::Table(t) - | sea_query::TableRef::SchemaTable(_, t) - | sea_query::TableRef::DatabaseSchemaTable(_, _, t) - | sea_query::TableRef::TableAlias(t, _) - | sea_query::TableRef::SchemaTableAlias(_, t, _) - | sea_query::TableRef::DatabaseSchemaTableAlias(_, _, t, _) => t.to_string(), - _ => unimplemented!(), - }, + Some(table_ref) => table_ref.sea_orm_table().to_string(), None => { return Err(Error::TransformError( "Table name should not be empty".into(), - )) + )); } }; let mut primary_keys: Vec = Vec::new(); - let columns: Vec = table_create + let mut columns: Vec = table_create .get_columns() .iter() .map(|col_def| { - let primary_key = col_def - .get_column_spec() - .iter() - .any(|spec| matches!(spec, ColumnSpec::PrimaryKey)); + let primary_key = col_def.get_column_spec().primary_key; if primary_key { primary_keys.push(PrimaryKey { name: col_def.get_column_name(), @@ -47,7 +36,7 @@ impl EntityTransformer { col_def.into() }) .map(|mut col: Column| { - col.unique = table_create + col.unique |= table_create .get_indexes() .iter() .filter(|index| index.is_unique_key()) @@ -57,8 +46,9 @@ impl EntityTransformer { > 0; col }) - .map(|col| { - if let sea_query::ColumnType::Enum { name, variants } = &col.col_type { + .inspect(|col| { + if let sea_query::ColumnType::Enum { name, variants } = col.get_inner_col_type() + { enums.insert( name.to_string(), ActiveEnum { @@ -67,16 +57,34 @@ impl EntityTransformer { }, ); } - col }) .collect(); + for index in table_create.get_indexes().iter() { + if index.is_unique_key() { + let col_names = index.get_index_spec().get_column_names(); + if col_names.len() > 1 { + if let Some(mut key_name) = index.get_index_spec().get_name() { + if let Some((_, suffix)) = key_name.rsplit_once('-') { + key_name = suffix; + } + for col_name in col_names { + for column in columns.iter_mut() { + if column.name == col_name { + column.unique_key = Some(key_name.to_owned()); + } + } + } + } + } + } + } let mut ref_table_counts: BTreeMap = BTreeMap::new(); let relations: Vec = table_create .get_foreign_key_create_stmts() .iter() .map(|fk_create_stmt| fk_create_stmt.get_foreign_key()) .map(|tbl_fk| { - let ref_tbl = unpack_table_ref(tbl_fk.get_ref_table().unwrap()); + let ref_tbl = tbl_fk.get_ref_table().unwrap().sea_orm_table().to_string(); if let Some(count) = ref_table_counts.get_mut(&ref_tbl) { if *count == 0 { *count = 1; @@ -147,13 +155,24 @@ impl EntityTransformer { break; } } + if rel.columns.len() == entity.primary_keys.len() { + let mut count_pk = 0; + for primary_key in entity.primary_keys.iter() { + if rel.columns.contains(&primary_key.name) { + count_pk += 1; + } + } + if count_pk == entity.primary_keys.len() { + unique = true; + } + } let rel_type = if unique { RelationType::HasOne } else { RelationType::HasMany }; rel.rel_type = rel_type; - rel.ref_table = table_name.clone(); + rel.ref_table = table_name.to_string(); rel.columns = Vec::new(); rel.ref_columns = Vec::new(); if let Some(vec) = inverse_relations.get_mut(&ref_table) { @@ -176,6 +195,17 @@ impl EntityTransformer { } } } + + // When codegen is fed with a subset of tables (e.g. via `sea-orm-cli generate entity --tables`), + // we must not generate relations that point to entities outside this set, otherwise it will + // produce invalid paths like `super::::Entity`. + let table_names: HashSet = entities.keys().cloned().collect(); + for entity in entities.values_mut() { + entity + .relations + .retain(|rel| rel.self_referencing || table_names.contains(&rel.ref_table)); + } + for table_name in entities.clone().keys() { let relations = match entities.get(table_name) { Some(entity) => { @@ -248,6 +278,7 @@ mod tests { use pretty_assertions::assert_eq; use proc_macro2::TokenStream; use sea_orm::{DbBackend, Schema}; + use sea_query::{ColumnDef, ForeignKey, Table}; use std::{ error::Error, io::{self, BufRead, BufReader}, @@ -359,47 +390,217 @@ mod tests { ) } - fn validate_compact_entities( - table_create_stmts: Vec, - files: Vec<(&str, &str)>, - ) -> Result<(), Box> { - let entities: HashMap<_, _> = EntityTransformer::transform(table_create_stmts)? + #[test] + fn test_indexes_transform() -> Result<(), Box> { + let schema = Schema::new(DbBackend::Postgres); + + validate_compact_entities( + vec![ + schema.create_table_with_index_from_entity( + crate::tests_cfg::compact::indexes::Entity, + ), + ], + vec![("indexes", include_str!("../tests_cfg/compact/indexes.rs"))], + )?; + + validate_dense_entities( + vec![ + schema + .create_table_with_index_from_entity(crate::tests_cfg::dense::indexes::Entity), + ], + vec![("indexes", include_str!("../tests_cfg/dense/indexes.rs"))], + )?; + + Ok(()) + } + + #[test] + fn filter_relations_to_missing_entities() -> Result<(), Box> { + let parent_stmt = || { + Table::create() + .table("parent") + .col( + ColumnDef::new("id") + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .to_owned() + }; + + let child_stmt = || { + Table::create() + .table("child") + .col( + ColumnDef::new("id") + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new("parent_id").integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-child-parent_id") + .from("child", "parent_id") + .to("parent", "id"), + ) + .to_owned() + }; + + let entities: HashMap<_, _> = + EntityTransformer::transform(vec![parent_stmt(), child_stmt()])? + .entities + .into_iter() + .map(|entity| (entity.table_name.clone(), entity)) + .collect(); + + let child = entities.get("child").expect("missing entity `child`"); + assert_eq!(child.relations.len(), 1); + assert_eq!(child.relations[0].ref_table, "parent"); + + let entities: HashMap<_, _> = EntityTransformer::transform(vec![child_stmt()])? .entities .into_iter() .map(|entity| (entity.table_name.clone(), entity)) .collect(); - for (entity_name, file_content) in files { - let entity = entities - .get(entity_name) - .expect("Forget to add entity to the list"); - - assert_eq!( - parse_from_file(file_content.as_bytes())?.to_string(), - EntityWriter::gen_compact_code_blocks( - entity, - &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, - &None, - false, - false, - &Default::default(), - &Default::default(), - false, + let child = entities.get("child").expect("missing entity `child`"); + assert!(child.relations.is_empty()); + + Ok(()) + } + + #[test] + fn filter_conjunct_relations_to_missing_entities() -> Result<(), Box> { + let user_stmt = || { + Table::create() + .table("user") + .col( + ColumnDef::new("id") + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .to_owned() + }; + + let role_stmt = || { + Table::create() + .table("role") + .col( + ColumnDef::new("id") + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .to_owned() + }; + + let user_role_stmt = || { + Table::create() + .table("user_role") + .col(ColumnDef::new("user_id").integer().not_null().primary_key()) + .col(ColumnDef::new("role_id").integer().not_null().primary_key()) + .foreign_key( + ForeignKey::create() + .name("fk-user_role-user_id") + .from("user_role", "user_id") + .to("user", "id"), ) + .foreign_key( + ForeignKey::create() + .name("fk-user_role-role_id") + .from("user_role", "role_id") + .to("role", "id"), + ) + .to_owned() + }; + + let entities: HashMap<_, _> = + EntityTransformer::transform(vec![user_stmt(), role_stmt(), user_role_stmt()])? + .entities .into_iter() - .skip(1) - .fold(TokenStream::new(), |mut acc, tok| { - acc.extend(tok); - acc - }) - .to_string() - ); - } + .map(|entity| (entity.table_name.clone(), entity)) + .collect(); + + let user = entities.get("user").expect("missing entity `user`"); + assert!(user.conjunct_relations.iter().any(|conjunct_relation| { + conjunct_relation.via == "user_role" && conjunct_relation.to == "role" + })); + + let entities: HashMap<_, _> = + EntityTransformer::transform(vec![user_stmt(), user_role_stmt()])? + .entities + .into_iter() + .map(|entity| (entity.table_name.clone(), entity)) + .collect(); + + let user = entities.get("user").expect("missing entity `user`"); + assert!(user.conjunct_relations.is_empty()); + + let user_role = entities + .get("user_role") + .expect("missing entity `user_role`"); + assert_eq!(user_role.relations.len(), 1); + assert_eq!(user_role.relations[0].ref_table, "user"); Ok(()) } + macro_rules! validate_entities_fn { + ($fn_name: ident, $method: ident) => { + fn $fn_name( + table_create_stmts: Vec, + files: Vec<(&str, &str)>, + ) -> Result<(), Box> { + let entities: HashMap<_, _> = EntityTransformer::transform(table_create_stmts)? + .entities + .into_iter() + .map(|entity| (entity.table_name.clone(), entity)) + .collect(); + + for (entity_name, file_content) in files { + let entity = entities + .get(entity_name) + .expect("Forget to add entity to the list"); + + assert_eq!( + parse_from_file(file_content.as_bytes())?.to_string(), + EntityWriter::$method( + entity, + &crate::WithSerde::None, + &Default::default(), + &None, + false, + false, + &Default::default(), + &Default::default(), + &Default::default(), + false, + true, + ) + .into_iter() + .skip(1) + .fold(TokenStream::new(), |mut acc, tok| { + acc.extend(tok); + acc + }) + .to_string() + ); + } + + Ok(()) + } + }; + } + + validate_entities_fn!(validate_compact_entities, gen_compact_code_blocks); + validate_entities_fn!(validate_dense_entities, gen_dense_code_blocks); + fn parse_from_file(inner: R) -> io::Result where R: io::Read, diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 36da98910d..b94b9aa5e0 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -1,4 +1,4 @@ -use crate::{util::escape_rust_keyword, ActiveEnum, Entity}; +use crate::{ActiveEnum, ColumnOption, Entity, util::escape_rust_keyword}; use heck::ToUpperCamelCase; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -6,6 +6,12 @@ use std::{collections::BTreeMap, str::FromStr}; use syn::{punctuated::Punctuated, token::Comma}; use tracing::info; +mod compact; +mod dense; +mod expanded; +mod frontend; +mod mermaid; + #[derive(Clone, Debug)] pub struct EntityWriter { pub(crate) entities: Vec, @@ -21,33 +27,75 @@ pub struct OutputFile { pub content: String, } -#[derive(PartialEq, Eq, Debug)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum WithPrelude { + #[default] + All, + None, + AllAllowUnusedImports, +} + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub enum WithSerde { + #[default] None, Serialize, Deserialize, Both, } -#[derive(Debug)] +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub enum DateTimeCrate { + #[default] Chrono, Time, } +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +pub enum BigIntegerType { + #[default] + I64, + I32, +} + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +pub enum EntityFormat { + #[default] + Compact, + Expanded, + Frontend, + Dense, +} + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +pub enum BannerVersion { + Off, + Major, + #[default] + Minor, + Patch, +} + #[derive(Debug)] pub struct EntityWriterContext { - pub(crate) expanded_format: bool, + pub(crate) entity_format: EntityFormat, + pub(crate) with_prelude: WithPrelude, pub(crate) with_serde: WithSerde, pub(crate) with_copy_enums: bool, pub(crate) date_time_crate: DateTimeCrate, + pub(crate) big_integer_type: BigIntegerType, pub(crate) schema_name: Option, pub(crate) lib: bool, pub(crate) serde_skip_hidden_column: bool, pub(crate) serde_skip_deserializing_primary_key: bool, pub(crate) model_extra_derives: TokenStream, pub(crate) model_extra_attributes: TokenStream, + pub(crate) enum_extra_derives: TokenStream, + pub(crate) enum_extra_attributes: TokenStream, + pub(crate) column_extra_derives: TokenStream, pub(crate) seaography: bool, + pub(crate) impl_active_model_behavior: bool, + pub(crate) banner_version: BannerVersion, } impl WithSerde { @@ -79,23 +127,23 @@ impl WithSerde { } } -/// Converts model_extra_derives argument to token stream -fn bonus_derive(model_extra_derives: I) -> TokenStream +/// Converts *_extra_derives argument to token stream +pub(crate) fn bonus_derive(extra_derives: I) -> TokenStream where T: Into, I: IntoIterator, { - model_extra_derives - .into_iter() - .map(Into::::into) - .fold(TokenStream::default(), |acc, derive| { + extra_derives.into_iter().map(Into::::into).fold( + TokenStream::default(), + |acc, derive| { let tokens: TokenStream = derive.parse().unwrap(); quote! { #acc, #tokens } - }) + }, + ) } -/// convert attributes argument to token stream -fn bonus_attributes(attributes: I) -> TokenStream +/// convert *_extra_attributes argument to token stream +pub(crate) fn bonus_attributes(attributes: I) -> TokenStream where T: Into, I: IntoIterator, @@ -112,6 +160,41 @@ where ) } +impl FromStr for WithPrelude { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "none" => Self::None, + "all-allow-unused-imports" => Self::AllAllowUnusedImports, + "all" => Self::All, + v => { + return Err(crate::Error::TransformError(format!( + "Unsupported enum variant '{v}'" + ))); + } + }) + } +} + +impl FromStr for EntityFormat { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "compact" => Self::Compact, + "expanded" => Self::Expanded, + "frontend" => Self::Frontend, + "dense" => Self::Dense, + v => { + return Err(crate::Error::TransformError(format!( + "Unsupported enum variant '{v}'" + ))); + } + }) + } +} + impl FromStr for WithSerde { type Err = crate::Error; @@ -124,7 +207,7 @@ impl FromStr for WithSerde { v => { return Err(crate::Error::TransformError(format!( "Unsupported enum variant '{v}'" - ))) + ))); } }) } @@ -133,30 +216,51 @@ impl FromStr for WithSerde { impl EntityWriterContext { #[allow(clippy::too_many_arguments)] pub fn new( - expanded_format: bool, + entity_format: EntityFormat, + with_prelude: WithPrelude, with_serde: WithSerde, with_copy_enums: bool, date_time_crate: DateTimeCrate, + big_integer_type: BigIntegerType, schema_name: Option, lib: bool, serde_skip_deserializing_primary_key: bool, serde_skip_hidden_column: bool, model_extra_derives: Vec, model_extra_attributes: Vec, + enum_extra_derives: Vec, + enum_extra_attributes: Vec, + column_extra_derives: Vec, seaography: bool, + impl_active_model_behavior: bool, + banner_version: BannerVersion, ) -> Self { Self { - expanded_format, + entity_format, + with_prelude, with_serde, with_copy_enums, date_time_crate, + big_integer_type, schema_name, lib, serde_skip_deserializing_primary_key, serde_skip_hidden_column, model_extra_derives: bonus_derive(model_extra_derives), model_extra_attributes: bonus_attributes(model_extra_attributes), + enum_extra_derives: bonus_derive(enum_extra_derives), + enum_extra_attributes: bonus_attributes(enum_extra_attributes), + column_extra_derives: bonus_derive(column_extra_derives), seaography, + impl_active_model_behavior, + banner_version, + } + } + + fn column_option(&self) -> ColumnOption { + ColumnOption { + date_time_crate: self.date_time_crate, + big_integer_type: self.big_integer_type, } } } @@ -165,12 +269,29 @@ impl EntityWriter { pub fn generate(self, context: &EntityWriterContext) -> WriterOutput { let mut files = Vec::new(); files.extend(self.write_entities(context)); - files.push(self.write_index_file(context.lib)); - files.push(self.write_prelude()); + let with_prelude = context.with_prelude != WithPrelude::None; + files.push(self.write_index_file( + context.lib, + with_prelude, + context.seaography, + context.banner_version, + )); + if with_prelude { + files.push(self.write_prelude( + context.with_prelude, + context.entity_format, + context.banner_version, + )); + } if !self.enums.is_empty() { - files.push( - self.write_sea_orm_active_enums(&context.with_serde, context.with_copy_enums), - ); + files.push(self.write_sea_orm_active_enums( + &context.with_serde, + context.with_copy_enums, + &context.enum_extra_derives, + &context.enum_extra_attributes, + context.entity_format, + context.banner_version, + )); } WriterOutput { files } } @@ -183,7 +304,7 @@ impl EntityWriter { let column_info = entity .columns .iter() - .map(|column| column.get_info(&context.date_time_crate)) + .map(|column| column.get_info(&context.column_option())) .collect::>(); // Serde must be enabled to use this let serde_skip_deserializing_primary_key = context @@ -201,30 +322,62 @@ impl EntityWriter { } let mut lines = Vec::new(); - Self::write_doc_comment(&mut lines); - let code_blocks = if context.expanded_format { + Self::write_doc_comment(&mut lines, context.banner_version); + let code_blocks = if context.entity_format == EntityFormat::Frontend { + Self::gen_frontend_code_blocks( + entity, + &context.with_serde, + &context.column_option(), + &context.schema_name, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + &context.model_extra_derives, + &context.model_extra_attributes, + &context.column_extra_derives, + context.seaography, + context.impl_active_model_behavior, + ) + } else if context.entity_format == EntityFormat::Expanded { Self::gen_expanded_code_blocks( entity, &context.with_serde, - &context.date_time_crate, + &context.column_option(), + &context.schema_name, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + &context.model_extra_derives, + &context.model_extra_attributes, + &context.column_extra_derives, + context.seaography, + context.impl_active_model_behavior, + ) + } else if context.entity_format == EntityFormat::Dense { + Self::gen_dense_code_blocks( + entity, + &context.with_serde, + &context.column_option(), &context.schema_name, serde_skip_deserializing_primary_key, serde_skip_hidden_column, &context.model_extra_derives, &context.model_extra_attributes, + &context.column_extra_derives, context.seaography, + context.impl_active_model_behavior, ) } else { Self::gen_compact_code_blocks( entity, &context.with_serde, - &context.date_time_crate, + &context.column_option(), &context.schema_name, serde_skip_deserializing_primary_key, serde_skip_hidden_column, &context.model_extra_derives, &context.model_extra_attributes, + &context.column_extra_derives, context.seaography, + context.impl_active_model_behavior, ) }; Self::write(&mut lines, code_blocks); @@ -236,17 +389,25 @@ impl EntityWriter { .collect() } - pub fn write_index_file(&self, lib: bool) -> OutputFile { + pub fn write_index_file( + &self, + lib: bool, + prelude: bool, + seaography: bool, + banner_version: BannerVersion, + ) -> OutputFile { let mut lines = Vec::new(); - Self::write_doc_comment(&mut lines); + Self::write_doc_comment(&mut lines, banner_version); let code_blocks: Vec = self.entities.iter().map(Self::gen_mod).collect(); - Self::write( - &mut lines, - vec![quote! { - pub mod prelude; - }], - ); - lines.push("".to_owned()); + if prelude { + Self::write( + &mut lines, + vec![quote! { + pub mod prelude; + }], + ); + lines.push("".to_owned()); + } Self::write(&mut lines, code_blocks); if !self.enums.is_empty() { Self::write( @@ -257,6 +418,12 @@ impl EntityWriter { ); } + if seaography { + lines.push("".to_owned()); + let ts = Self::gen_seaography_entity_mod(&self.entities, &self.enums); + Self::write(&mut lines, vec![ts]); + } + let file_name = match lib { true => "lib.rs".to_owned(), false => "mod.rs".to_owned(), @@ -268,10 +435,28 @@ impl EntityWriter { } } - pub fn write_prelude(&self) -> OutputFile { + pub fn write_prelude( + &self, + with_prelude: WithPrelude, + entity_format: EntityFormat, + banner_version: BannerVersion, + ) -> OutputFile { let mut lines = Vec::new(); - Self::write_doc_comment(&mut lines); - let code_blocks = self.entities.iter().map(Self::gen_prelude_use).collect(); + Self::write_doc_comment(&mut lines, banner_version); + if with_prelude == WithPrelude::AllAllowUnusedImports { + Self::write_allow_unused_imports(&mut lines) + } + let code_blocks = self + .entities + .iter() + .map({ + if entity_format == EntityFormat::Frontend { + Self::gen_prelude_use_model + } else { + Self::gen_prelude_use + } + }) + .collect(); Self::write(&mut lines, code_blocks); OutputFile { name: "prelude.rs".to_owned(), @@ -283,15 +468,31 @@ impl EntityWriter { &self, with_serde: &WithSerde, with_copy_enums: bool, + extra_derives: &TokenStream, + extra_attributes: &TokenStream, + entity_format: EntityFormat, + banner_version: BannerVersion, ) -> OutputFile { let mut lines = Vec::new(); - Self::write_doc_comment(&mut lines); - Self::write(&mut lines, vec![Self::gen_import(with_serde)]); + Self::write_doc_comment(&mut lines, banner_version); + if entity_format == EntityFormat::Frontend { + Self::write(&mut lines, vec![Self::gen_import_serde(with_serde)]); + } else { + Self::write(&mut lines, vec![Self::gen_import(with_serde)]); + } lines.push("".to_owned()); let code_blocks = self .enums .values() - .map(|active_enum| active_enum.impl_active_enum(with_serde, with_copy_enums)) + .map(|active_enum| { + active_enum.impl_active_enum( + with_serde, + with_copy_enums, + extra_derives, + extra_attributes, + entity_format, + ) + }) .collect(); Self::write(&mut lines, code_blocks); OutputFile { @@ -309,117 +510,63 @@ impl EntityWriter { ); } - pub fn write_doc_comment(lines: &mut Vec) { + pub fn write_doc_comment(lines: &mut Vec, banner_version: BannerVersion) { let ver = env!("CARGO_PKG_VERSION"); + let version_str = match banner_version { + BannerVersion::Off => String::new(), + BannerVersion::Patch => ver.to_owned(), + _ => { + let parts: Vec<&str> = ver.split('.').collect(); + match banner_version { + BannerVersion::Major => { + parts.first().map(|x| (*x).to_owned()).unwrap_or_default() + } + BannerVersion::Minor => { + if parts.len() >= 2 { + format!("{}.{}", parts[0], parts[1]) + } else { + ver.to_owned() + } + } + _ => unreachable!(), + } + } + }; let comments = vec![format!( - "//! `SeaORM` Entity. Generated by sea-orm-codegen {ver}" + "//! `SeaORM` Entity, @generated by sea-orm-codegen {version_str}" )]; lines.extend(comments); lines.push("".to_owned()); } - #[allow(clippy::too_many_arguments)] - pub fn gen_expanded_code_blocks( - entity: &Entity, - with_serde: &WithSerde, - date_time_crate: &DateTimeCrate, - schema_name: &Option, - serde_skip_deserializing_primary_key: bool, - serde_skip_hidden_column: bool, - model_extra_derives: &TokenStream, - model_extra_attributes: &TokenStream, - seaography: bool, - ) -> Vec { - let mut imports = Self::gen_import(with_serde); - imports.extend(Self::gen_import_active_enum(entity)); - let mut code_blocks = vec![ - imports, - Self::gen_entity_struct(), - Self::gen_impl_entity_name(entity, schema_name), - Self::gen_model_struct( - entity, - with_serde, - date_time_crate, - serde_skip_deserializing_primary_key, - serde_skip_hidden_column, - model_extra_derives, - model_extra_attributes, - ), - Self::gen_column_enum(entity), - Self::gen_primary_key_enum(entity), - Self::gen_impl_primary_key(entity, date_time_crate), - Self::gen_relation_enum(entity), - Self::gen_impl_column_trait(entity), - Self::gen_impl_relation_trait(entity), - ]; - code_blocks.extend(Self::gen_impl_related(entity)); - code_blocks.extend(Self::gen_impl_conjunct_related(entity)); - code_blocks.extend([Self::gen_impl_active_model_behavior()]); - if seaography { - code_blocks.extend([Self::gen_related_entity(entity)]); - } - code_blocks - } - - #[allow(clippy::too_many_arguments)] - pub fn gen_compact_code_blocks( - entity: &Entity, - with_serde: &WithSerde, - date_time_crate: &DateTimeCrate, - schema_name: &Option, - serde_skip_deserializing_primary_key: bool, - serde_skip_hidden_column: bool, - model_extra_derives: &TokenStream, - model_extra_attributes: &TokenStream, - seaography: bool, - ) -> Vec { - let mut imports = Self::gen_import(with_serde); - imports.extend(Self::gen_import_active_enum(entity)); - let mut code_blocks = vec![ - imports, - Self::gen_compact_model_struct( - entity, - with_serde, - date_time_crate, - schema_name, - serde_skip_deserializing_primary_key, - serde_skip_hidden_column, - model_extra_derives, - model_extra_attributes, - ), - Self::gen_compact_relation_enum(entity), - ]; - code_blocks.extend(Self::gen_impl_related(entity)); - code_blocks.extend(Self::gen_impl_conjunct_related(entity)); - code_blocks.extend([Self::gen_impl_active_model_behavior()]); - if seaography { - code_blocks.extend([Self::gen_related_entity(entity)]); - } - code_blocks + pub fn write_allow_unused_imports(lines: &mut Vec) { + lines.extend(vec!["#![allow(unused_imports)]".to_string()]); + lines.push("".to_owned()); } pub fn gen_import(with_serde: &WithSerde) -> TokenStream { - let prelude_import = quote!( + let serde_import = Self::gen_import_serde(with_serde); + quote! { use sea_orm::entity::prelude::*; - ); + #serde_import + } + } + pub fn gen_import_serde(with_serde: &WithSerde) -> TokenStream { match with_serde { - WithSerde::None => prelude_import, + WithSerde::None => Default::default(), WithSerde::Serialize => { quote! { - #prelude_import use serde::Serialize; } } WithSerde::Deserialize => { quote! { - #prelude_import use serde::Deserialize; } } WithSerde::Both => { quote! { - #prelude_import use serde::{Deserialize,Serialize}; } } @@ -444,7 +591,7 @@ impl EntityWriter { }; let table_name = entity.table_name.as_str(); let table_name = quote! { - fn table_name(&self) -> &str { + fn table_name(&self) -> &'static str { #table_name } }; @@ -460,48 +607,26 @@ impl EntityWriter { entity .columns .iter() - .fold(TokenStream::new(), |mut ts, col| { - if let sea_query::ColumnType::Enum { name, .. } = &col.col_type { - let enum_name = format_ident!("{}", name.to_string().to_upper_camel_case()); - ts.extend([quote! { - use super::sea_orm_active_enums::#enum_name; - }]); - } - ts - }) - } - - pub fn gen_model_struct( - entity: &Entity, - with_serde: &WithSerde, - date_time_crate: &DateTimeCrate, - serde_skip_deserializing_primary_key: bool, - serde_skip_hidden_column: bool, - model_extra_derives: &TokenStream, - model_extra_attributes: &TokenStream, - ) -> TokenStream { - let column_names_snake_case = entity.get_column_names_snake_case(); - let column_rs_types = entity.get_column_rs_types(date_time_crate); - let if_eq_needed = entity.get_eq_needed(); - let serde_attributes = entity.get_column_serde_attributes( - serde_skip_deserializing_primary_key, - serde_skip_hidden_column, - ); - let extra_derive = with_serde.extra_derive(); - - quote! { - #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel #if_eq_needed #extra_derive #model_extra_derives)] - #model_extra_attributes - pub struct Model { - #( - #serde_attributes - pub #column_names_snake_case: #column_rs_types, - )* - } - } + .fold( + (TokenStream::new(), Vec::new()), + |(mut ts, mut enums), col| { + if let sea_query::ColumnType::Enum { name, .. } = col.get_inner_col_type() { + if !enums.contains(&name) { + enums.push(name); + let enum_name = + format_ident!("{}", name.to_string().to_upper_camel_case()); + ts.extend([quote! { + use super::sea_orm_active_enums::#enum_name; + }]); + } + } + (ts, enums) + }, + ) + .0 } - pub fn gen_column_enum(entity: &Entity) -> TokenStream { + pub fn gen_column_enum(entity: &Entity, column_extra_derives: &TokenStream) -> TokenStream { let column_variants = entity.columns.iter().map(|col| { let variant = col.get_name_camel_case(); let mut variant = quote! { #variant }; @@ -515,7 +640,7 @@ impl EntityWriter { variant }); quote! { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn #column_extra_derives)] pub enum Column { #(#column_variants,)* } @@ -532,9 +657,9 @@ impl EntityWriter { } } - pub fn gen_impl_primary_key(entity: &Entity, date_time_crate: &DateTimeCrate) -> TokenStream { + pub fn gen_impl_primary_key(entity: &Entity, column_option: &ColumnOption) -> TokenStream { let primary_key_auto_increment = entity.get_primary_key_auto_increment(); - let value_type = entity.get_primary_key_rs_type(date_time_crate); + let value_type = entity.get_primary_key_rs_type(column_option); quote! { impl PrimaryKeyTrait for PrimaryKey { type ValueType = #value_type; @@ -662,7 +787,7 @@ impl EntityWriter { .collect() } - pub fn gen_impl_active_model_behavior() -> TokenStream { + pub fn impl_active_model_behavior() -> TokenStream { quote! { impl ActiveModelBehavior for ActiveModel {} } @@ -678,6 +803,48 @@ impl EntityWriter { } } + pub fn gen_seaography_entity_mod( + entities: &[Entity], + enums: &BTreeMap, + ) -> TokenStream { + let mut ts = TokenStream::new(); + for entity in entities { + let table_name_snake_case_ident = format_ident!( + "{}", + escape_rust_keyword(entity.get_table_name_snake_case_ident()) + ); + ts = quote! { + #ts + #table_name_snake_case_ident, + } + } + ts = quote! { + seaography::register_entity_modules!([ + #ts + ]); + }; + + let mut enum_ts = TokenStream::new(); + for active_enum in enums.values() { + let enum_name = &active_enum.enum_name.to_string(); + let enum_iden = format_ident!("{}", enum_name.to_upper_camel_case()); + enum_ts = quote! { + #enum_ts + sea_orm_active_enums::#enum_iden, + } + } + if !enum_ts.is_empty() { + ts = quote! { + #ts + + seaography::register_active_enums!([ + #enum_ts + ]); + }; + } + ts + } + pub fn gen_prelude_use(entity: &Entity) -> TokenStream { let table_name_snake_case_ident = entity.get_table_name_snake_case_ident(); let table_name_camel_case_ident = entity.get_table_name_camel_case_ident(); @@ -686,137 +853,38 @@ impl EntityWriter { } } - #[allow(clippy::too_many_arguments)] - pub fn gen_compact_model_struct( - entity: &Entity, - with_serde: &WithSerde, - date_time_crate: &DateTimeCrate, - schema_name: &Option, - serde_skip_deserializing_primary_key: bool, - serde_skip_hidden_column: bool, - model_extra_derives: &TokenStream, - model_extra_attributes: &TokenStream, - ) -> TokenStream { - let table_name = entity.table_name.as_str(); - let column_names_snake_case = entity.get_column_names_snake_case(); - let column_rs_types = entity.get_column_rs_types(date_time_crate); - let if_eq_needed = entity.get_eq_needed(); - let primary_keys: Vec = entity - .primary_keys - .iter() - .map(|pk| pk.name.clone()) - .collect(); - let attrs: Vec = entity - .columns - .iter() - .map(|col| { - let mut attrs: Punctuated<_, Comma> = Punctuated::new(); - let is_primary_key = primary_keys.contains(&col.name); - if !col.is_snake_case_name() { - let column_name = &col.name; - attrs.push(quote! { column_name = #column_name }); - } - if is_primary_key { - attrs.push(quote! { primary_key }); - if !col.auto_increment { - attrs.push(quote! { auto_increment = false }); - } - } - if let Some(ts) = col.get_col_type_attrs() { - attrs.extend([ts]); - if !col.not_null { - attrs.push(quote! { nullable }); - } - }; - if col.unique { - attrs.push(quote! { unique }); - } - let mut ts = quote! {}; - if !attrs.is_empty() { - for (i, attr) in attrs.into_iter().enumerate() { - if i > 0 { - ts = quote! { #ts, }; - } - ts = quote! { #ts #attr }; - } - ts = quote! { #[sea_orm(#ts)] }; - } - let serde_attribute = col.get_serde_attribute( - is_primary_key, - serde_skip_deserializing_primary_key, - serde_skip_hidden_column, - ); - ts = quote! { - #ts - #serde_attribute - }; - ts - }) - .collect(); - let schema_name = match Self::gen_schema_name(schema_name) { - Some(schema_name) => quote! { - schema_name = #schema_name, - }, - None => quote! {}, - }; - let extra_derive = with_serde.extra_derive(); - - quote! { - #[derive(Clone, Debug, PartialEq, DeriveEntityModel #if_eq_needed #extra_derive #model_extra_derives)] - #[sea_orm( - #schema_name - table_name = #table_name - )] - #model_extra_attributes - pub struct Model { - #( - #attrs - pub #column_names_snake_case: #column_rs_types, - )* - } - } - } - - pub fn gen_compact_relation_enum(entity: &Entity) -> TokenStream { - let relation_enum_name = entity.get_relation_enum_name(); - let attrs = entity.get_relation_attrs(); + pub fn gen_prelude_use_model(entity: &Entity) -> TokenStream { + let table_name_snake_case_ident = entity.get_table_name_snake_case_ident(); + let table_name_camel_case_ident = entity.get_table_name_camel_case_ident(); quote! { - #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] - pub enum Relation { - #( - #attrs - #relation_enum_name, - )* - } + pub use super::#table_name_snake_case_ident::Model as #table_name_camel_case_ident; } } pub fn gen_schema_name(schema_name: &Option) -> Option { - match schema_name { - Some(schema_name) => { - if schema_name != "public" { - Some(quote! { #schema_name }) - } else { - None - } - } - None => None, - } + schema_name + .as_ref() + .map(|schema_name| quote! { #schema_name }) } } #[cfg(test)] mod tests { use crate::{ - entity::writer::{bonus_attributes, bonus_derive}, - Column, ConjunctRelation, DateTimeCrate, Entity, EntityWriter, PrimaryKey, Relation, + Column, ColumnOption, ConjunctRelation, Entity, EntityWriter, PrimaryKey, Relation, RelationType, WithSerde, + entity::writer::{bonus_attributes, bonus_derive}, }; use pretty_assertions::assert_eq; use proc_macro2::TokenStream; - use sea_query::{ColumnType, ForeignKeyAction, RcOrArc}; + use quote::quote; + use sea_query::{Alias, ColumnType, ForeignKeyAction, RcOrArc, SeaRc, StringLen}; use std::io::{self, BufRead, BufReader, Read}; + fn default_column_option() -> ColumnOption { + Default::default() + } + fn setup() -> Vec { vec![ Entity { @@ -828,6 +896,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), @@ -835,6 +904,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![Relation { @@ -865,6 +935,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "filling_id".to_owned(), @@ -872,6 +943,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, ], relations: vec![ @@ -908,6 +980,55 @@ mod tests { }, ], }, + Entity { + table_name: "cake_filling_price".to_owned(), + columns: vec![ + Column { + name: "cake_id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "filling_id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "price".to_owned(), + col_type: ColumnType::Decimal(None), + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![Relation { + ref_table: "cake_filling".to_owned(), + columns: vec!["cake_id".to_owned(), "filling_id".to_owned()], + ref_columns: vec!["cake_id".to_owned(), "filling_id".to_owned()], + rel_type: RelationType::BelongsTo, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }], + conjunct_relations: vec![], + primary_keys: vec![ + PrimaryKey { + name: "cake_id".to_owned(), + }, + PrimaryKey { + name: "filling_id".to_owned(), + }, + ], + }, Entity { table_name: "filling".to_owned(), columns: vec![ @@ -917,13 +1038,15 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), - col_type: ColumnType::String(Some(255)), + col_type: ColumnType::String(StringLen::N(255)), auto_increment: false, not_null: true, unique: false, + unique_key: None, }, ], relations: vec![], @@ -944,13 +1067,15 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), - col_type: ColumnType::String(Some(255)), + col_type: ColumnType::String(StringLen::N(255)), auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "cake_id".to_owned(), @@ -958,6 +1083,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![ @@ -998,13 +1124,15 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "_name_".to_owned(), - col_type: ColumnType::String(Some(255)), + col_type: ColumnType::String(StringLen::N(255)), auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "fruitId".to_owned(), @@ -1012,6 +1140,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![Relation { @@ -1039,6 +1168,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "testing".to_owned(), @@ -1046,6 +1176,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "rust".to_owned(), @@ -1053,6 +1184,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "keywords".to_owned(), @@ -1060,6 +1192,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "type".to_owned(), @@ -1067,6 +1200,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "typeof".to_owned(), @@ -1074,6 +1208,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "crate".to_owned(), @@ -1081,6 +1216,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "self".to_owned(), @@ -1088,6 +1224,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "self_id1".to_owned(), @@ -1095,6 +1232,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "self_id2".to_owned(), @@ -1102,6 +1240,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "fruit_id1".to_owned(), @@ -1109,6 +1248,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "fruit_id2".to_owned(), @@ -1116,6 +1256,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "cake_id".to_owned(), @@ -1123,6 +1264,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, ], relations: vec![ @@ -1196,6 +1338,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), @@ -1203,6 +1346,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, Column { name: "price".to_owned(), @@ -1210,6 +1354,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![Relation { @@ -1240,6 +1385,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), @@ -1247,6 +1393,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, Column { name: "price".to_owned(), @@ -1254,6 +1401,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![Relation { @@ -1284,6 +1432,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "integers".to_owned(), @@ -1291,6 +1440,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "integers_opt".to_owned(), @@ -1298,6 +1448,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![], @@ -1315,6 +1466,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "floats".to_owned(), @@ -1322,6 +1474,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "doubles".to_owned(), @@ -1329,6 +1482,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, ], relations: vec![], @@ -1337,18 +1491,121 @@ mod tests { name: "id".to_owned(), }], }, - ] - } - - fn parse_from_file(inner: R) -> io::Result - where - R: Read, - { - let mut reader = BufReader::new(inner); - let mut lines: Vec = Vec::new(); - - reader.read_until(b';', &mut Vec::new())?; - + Entity { + table_name: "parent".to_owned(), + columns: vec![ + Column { + name: "id1".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "id2".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![Relation { + ref_table: "child".to_owned(), + columns: vec![], + ref_columns: vec![], + rel_type: RelationType::HasMany, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }], + conjunct_relations: vec![], + primary_keys: vec![ + PrimaryKey { + name: "id1".to_owned(), + }, + PrimaryKey { + name: "id2".to_owned(), + }, + ], + }, + Entity { + table_name: "child".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "parent_id1".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "parent_id2".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![Relation { + ref_table: "parent".to_owned(), + columns: vec!["parent_id1".to_owned(), "parent_id2".to_owned()], + ref_columns: vec!["id1".to_owned(), "id2".to_owned()], + rel_type: RelationType::BelongsTo, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }], + conjunct_relations: vec![], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + ] + } + + fn parse_from_file(inner: R) -> io::Result + where + R: Read, + { + let mut reader = BufReader::new(inner); + let mut lines: Vec = Vec::new(); + + reader.read_until(b';', &mut Vec::new())?; + + let mut line = String::new(); + while reader.read_line(&mut line)? > 0 { + lines.push(line.to_owned()); + line.clear(); + } + let content = lines.join(""); + Ok(content.parse().unwrap()) + } + + fn parse_from_frontend_file(inner: R) -> io::Result + where + R: Read, + { + let mut reader = BufReader::new(inner); + let mut lines: Vec = Vec::new(); + + reader.read_until(b'\n', &mut Vec::new())?; + let mut line = String::new(); while reader.read_line(&mut line)? > 0 { lines.push(line.to_owned()); @@ -1361,9 +1618,10 @@ mod tests { #[test] fn test_gen_expanded_code_blocks() -> io::Result<()> { let entities = setup(); - const ENTITY_FILES: [&str; 10] = [ + const ENTITY_FILES: [&str; 13] = [ include_str!("../../tests/expanded/cake.rs"), include_str!("../../tests/expanded/cake_filling.rs"), + include_str!("../../tests/expanded/cake_filling_price.rs"), include_str!("../../tests/expanded/filling.rs"), include_str!("../../tests/expanded/fruit.rs"), include_str!("../../tests/expanded/vendor.rs"), @@ -1372,10 +1630,13 @@ mod tests { include_str!("../../tests/expanded/cake_with_double.rs"), include_str!("../../tests/expanded/collection.rs"), include_str!("../../tests/expanded/collection_float.rs"), + include_str!("../../tests/expanded/parent.rs"), + include_str!("../../tests/expanded/child.rs"), ]; - const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 10] = [ + const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 13] = [ include_str!("../../tests/expanded_with_schema_name/cake.rs"), include_str!("../../tests/expanded_with_schema_name/cake_filling.rs"), + include_str!("../../tests/expanded_with_schema_name/cake_filling_price.rs"), include_str!("../../tests/expanded_with_schema_name/filling.rs"), include_str!("../../tests/expanded_with_schema_name/fruit.rs"), include_str!("../../tests/expanded_with_schema_name/vendor.rs"), @@ -1384,6 +1645,8 @@ mod tests { include_str!("../../tests/expanded_with_schema_name/cake_with_double.rs"), include_str!("../../tests/expanded_with_schema_name/collection.rs"), include_str!("../../tests/expanded_with_schema_name/collection_float.rs"), + include_str!("../../tests/expanded_with_schema_name/parent.rs"), + include_str!("../../tests/expanded_with_schema_name/child.rs"), ]; assert_eq!(entities.len(), ENTITY_FILES.len()); @@ -1394,34 +1657,15 @@ mod tests { EntityWriter::gen_expanded_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), - false - ) - .into_iter() - .skip(1) - .fold(TokenStream::new(), |mut acc, tok| { - acc.extend(tok); - acc - }) - .to_string() - ); - assert_eq!( - parse_from_file(ENTITY_FILES[i].as_bytes())?.to_string(), - EntityWriter::gen_expanded_code_blocks( - entity, - &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, - &Some("public".to_owned()), - false, - false, - &TokenStream::new(), &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -1436,13 +1680,15 @@ mod tests { EntityWriter::gen_expanded_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, + &default_column_option(), &Some("schema_name".to_owned()), false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -1460,9 +1706,10 @@ mod tests { #[test] fn test_gen_compact_code_blocks() -> io::Result<()> { let entities = setup(); - const ENTITY_FILES: [&str; 10] = [ + const ENTITY_FILES: [&str; 13] = [ include_str!("../../tests/compact/cake.rs"), include_str!("../../tests/compact/cake_filling.rs"), + include_str!("../../tests/compact/cake_filling_price.rs"), include_str!("../../tests/compact/filling.rs"), include_str!("../../tests/compact/fruit.rs"), include_str!("../../tests/compact/vendor.rs"), @@ -1471,10 +1718,13 @@ mod tests { include_str!("../../tests/compact/cake_with_double.rs"), include_str!("../../tests/compact/collection.rs"), include_str!("../../tests/compact/collection_float.rs"), + include_str!("../../tests/compact/parent.rs"), + include_str!("../../tests/compact/child.rs"), ]; - const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 10] = [ + const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 13] = [ include_str!("../../tests/compact_with_schema_name/cake.rs"), include_str!("../../tests/compact_with_schema_name/cake_filling.rs"), + include_str!("../../tests/compact_with_schema_name/cake_filling_price.rs"), include_str!("../../tests/compact_with_schema_name/filling.rs"), include_str!("../../tests/compact_with_schema_name/fruit.rs"), include_str!("../../tests/compact_with_schema_name/vendor.rs"), @@ -1483,6 +1733,8 @@ mod tests { include_str!("../../tests/compact_with_schema_name/cake_with_double.rs"), include_str!("../../tests/compact_with_schema_name/collection.rs"), include_str!("../../tests/compact_with_schema_name/collection_float.rs"), + include_str!("../../tests/compact_with_schema_name/parent.rs"), + include_str!("../../tests/compact_with_schema_name/child.rs"), ]; assert_eq!(entities.len(), ENTITY_FILES.len()); @@ -1493,13 +1745,15 @@ mod tests { EntityWriter::gen_compact_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -1510,17 +1764,84 @@ mod tests { .to_string() ); assert_eq!( - parse_from_file(ENTITY_FILES[i].as_bytes())?.to_string(), + parse_from_file(ENTITY_FILES_WITH_SCHEMA_NAME[i].as_bytes())?.to_string(), EntityWriter::gen_compact_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, - &Some("public".to_owned()), + &default_column_option(), + &Some("schema_name".to_owned()), + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + ) + .into_iter() + .skip(1) + .fold(TokenStream::new(), |mut acc, tok| { + acc.extend(tok); + acc + }) + .to_string() + ); + } + + Ok(()) + } + + #[test] + fn test_gen_frontend_code_blocks() -> io::Result<()> { + let entities = setup(); + const ENTITY_FILES: [&str; 13] = [ + include_str!("../../tests/frontend/cake.rs"), + include_str!("../../tests/frontend/cake_filling.rs"), + include_str!("../../tests/frontend/cake_filling_price.rs"), + include_str!("../../tests/frontend/filling.rs"), + include_str!("../../tests/frontend/fruit.rs"), + include_str!("../../tests/frontend/vendor.rs"), + include_str!("../../tests/frontend/rust_keyword.rs"), + include_str!("../../tests/frontend/cake_with_float.rs"), + include_str!("../../tests/frontend/cake_with_double.rs"), + include_str!("../../tests/frontend/collection.rs"), + include_str!("../../tests/frontend/collection_float.rs"), + include_str!("../../tests/frontend/parent.rs"), + include_str!("../../tests/frontend/child.rs"), + ]; + const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 13] = [ + include_str!("../../tests/frontend_with_schema_name/cake.rs"), + include_str!("../../tests/frontend_with_schema_name/cake_filling.rs"), + include_str!("../../tests/frontend_with_schema_name/cake_filling_price.rs"), + include_str!("../../tests/frontend_with_schema_name/filling.rs"), + include_str!("../../tests/frontend_with_schema_name/fruit.rs"), + include_str!("../../tests/frontend_with_schema_name/vendor.rs"), + include_str!("../../tests/frontend_with_schema_name/rust_keyword.rs"), + include_str!("../../tests/frontend_with_schema_name/cake_with_float.rs"), + include_str!("../../tests/frontend_with_schema_name/cake_with_double.rs"), + include_str!("../../tests/frontend_with_schema_name/collection.rs"), + include_str!("../../tests/frontend_with_schema_name/collection_float.rs"), + include_str!("../../tests/frontend_with_schema_name/parent.rs"), + include_str!("../../tests/frontend_with_schema_name/child.rs"), + ]; + + assert_eq!(entities.len(), ENTITY_FILES.len()); + + for (i, entity) in entities.iter().enumerate() { + assert_eq!( + dbg!(parse_from_frontend_file(ENTITY_FILES[i].as_bytes())?.to_string()), + EntityWriter::gen_frontend_code_blocks( + entity, + &crate::WithSerde::None, + &default_column_option(), + &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -1531,17 +1852,19 @@ mod tests { .to_string() ); assert_eq!( - parse_from_file(ENTITY_FILES_WITH_SCHEMA_NAME[i].as_bytes())?.to_string(), - EntityWriter::gen_compact_code_blocks( + parse_from_frontend_file(ENTITY_FILES_WITH_SCHEMA_NAME[i].as_bytes())?.to_string(), + EntityWriter::gen_frontend_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, + &default_column_option(), &Some("schema_name".to_owned()), false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -1568,13 +1891,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1584,13 +1909,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::Serialize, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1600,13 +1927,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::Deserialize, - &DateTimeCrate::Chrono, + &default_column_option(), &None, true, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1614,13 +1943,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::Both, - &DateTimeCrate::Chrono, + &default_column_option(), &None, true, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); @@ -1630,13 +1961,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1646,13 +1979,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::Serialize, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1662,13 +1997,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::Deserialize, - &DateTimeCrate::Chrono, + &default_column_option(), &None, true, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1676,13 +2013,85 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::Both, - &DateTimeCrate::Chrono, + &default_column_option(), + &None, + true, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + + // Frontend code blocks + assert_eq!( + comparable_file_string(include_str!("../../tests/frontend_with_serde/cake_none.rs"))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_serde/cake_serialize.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::Serialize, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_serde/cake_deserialize.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::Deserialize, + &default_column_option(), + &None, + true, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!("../../tests/frontend_with_serde/cake_both.rs"))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::Both, + &default_column_option(), &None, true, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); @@ -1700,6 +2109,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "name".to_owned(), @@ -1707,6 +2117,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, Column { name: "base_id".to_owned(), @@ -1714,6 +2125,7 @@ mod tests { auto_increment: false, not_null: false, unique: false, + unique_key: None, }, ], relations: vec![ @@ -1757,12 +2169,14 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), + true, true, )) ); @@ -1773,54 +2187,132 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), + true, true, )) ); - Ok(()) - } - - #[test] - fn test_gen_with_derives() -> io::Result<()> { - let mut cake_entity = setup().get_mut(0).unwrap().clone(); - - assert_eq!(cake_entity.get_table_name_snake_case(), "cake"); - - // Compact code blocks + // Frontend code blocks assert_eq!( - comparable_file_string(include_str!( - "../../tests/compact_with_derives/cake_none.rs" - ))?, - generated_to_string(EntityWriter::gen_compact_code_blocks( + comparable_file_string(include_str!("../../tests/with_seaography/cake_frontend.rs"))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), - false, + &TokenStream::new(), + true, + true, )) ); - assert_eq!( - comparable_file_string(include_str!("../../tests/compact_with_derives/cake_one.rs"))?, - generated_to_string(EntityWriter::gen_compact_code_blocks( - &cake_entity, - &WithSerde::None, - &DateTimeCrate::Chrono, - &None, + + Ok(()) + } + + #[test] + fn test_gen_with_seaography_mod() -> io::Result<()> { + use crate::ActiveEnum; + use sea_query::IntoIden; + + let entities = setup(); + let enums = vec![ + ( + "coinflip_result_type", + ActiveEnum { + enum_name: Alias::new("coinflip_result_type").into_iden(), + values: vec!["HEADS", "TAILS"] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + }, + ), + ( + "media_type", + ActiveEnum { + enum_name: Alias::new("media_type").into_iden(), + values: vec![ + "UNKNOWN", + "BITMAP", + "DRAWING", + "AUDIO", + "VIDEO", + "MULTIMEDIA", + "OFFICE", + "TEXT", + "EXECUTABLE", + "ARCHIVE", + "3D", + ] + .into_iter() + .map(|variant| Alias::new(variant).into_iden()) + .collect(), + }, + ), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(); + + assert_eq!( + comparable_file_string(include_str!("../../tests/with_seaography/mod.rs"))?, + generated_to_string(vec![EntityWriter::gen_seaography_entity_mod( + &entities, &enums, + )]) + ); + + Ok(()) + } + + #[test] + fn test_gen_with_derives() -> io::Result<()> { + let mut cake_entity = setup().get_mut(0).unwrap().clone(); + + assert_eq!(cake_entity.get_table_name_snake_case(), "cake"); + + // Compact code blocks + assert_eq!( + comparable_file_string(include_str!( + "../../tests/compact_with_derives/cake_none.rs" + ))?, + generated_to_string(EntityWriter::gen_compact_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!("../../tests/compact_with_derives/cake_one.rs"))?, + generated_to_string(EntityWriter::gen_compact_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, false, false, &bonus_derive(["ts_rs::TS"]), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1830,13 +2322,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &bonus_derive(["ts_rs::TS", "utoipa::ToSchema"]), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); @@ -1848,13 +2342,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1864,13 +2360,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &bonus_derive(["ts_rs::TS"]), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -1880,13 +2378,71 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &bonus_derive(["ts_rs::TS", "utoipa::ToSchema"]), &TokenStream::new(), + &TokenStream::new(), false, + true, + )) + ); + + // Frontend code blocks + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_derives/cake_none.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_derives/cake_one.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &bonus_derive(["ts_rs::TS"]), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_derives/cake_multiple.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &bonus_derive(["ts_rs::TS", "utoipa::ToSchema"]), + &TokenStream::new(), + &TokenStream::new(), + false, + true, )) ); @@ -1913,6 +2469,63 @@ mod tests { ), Box::new(EntityWriter::gen_expanded_code_blocks), )?; + assert_serde_variant_results( + &cake_entity, + &( + include_str!( + "../../tests/frontend_with_serde/cake_serialize_with_hidden_column.rs" + ), + WithSerde::Serialize, + None, + ), + Box::new(EntityWriter::gen_frontend_code_blocks), + )?; + + Ok(()) + } + + #[test] + fn test_gen_with_column_derives() -> io::Result<()> { + let cake_entity = setup().get_mut(0).unwrap().clone(); + + assert_eq!(cake_entity.get_table_name_snake_case(), "cake"); + + assert_eq!( + comparable_file_string(include_str!( + "../../tests/expanded_with_column_derives/cake_one.rs" + ))?, + generated_to_string(EntityWriter::gen_expanded_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &bonus_derive(["async_graphql::Enum"]), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/expanded_with_column_derives/cake_multiple.rs" + ))?, + generated_to_string(EntityWriter::gen_expanded_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &bonus_derive(["async_graphql::Enum", "Eq", "PartialEq"]), + false, + true, + )) + ); Ok(()) } @@ -1925,12 +2538,14 @@ mod tests { dyn Fn( &Entity, &WithSerde, - &DateTimeCrate, + &ColumnOption, &Option, bool, bool, &TokenStream, &TokenStream, + &TokenStream, + bool, bool, ) -> Vec, >, @@ -1956,13 +2571,15 @@ mod tests { let generated = generator( cake_entity, &entity_serde_variant.1, - &DateTimeCrate::Chrono, + &default_column_option(), &entity_serde_variant.2, serde_skip_deserializing_primary_key, serde_skip_hidden_column, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .fold(TokenStream::new(), |mut acc, tok| { @@ -1988,13 +2605,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -2004,13 +2623,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &bonus_attributes([r#"serde(rename_all = "camelCase")"#]), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -2020,13 +2641,15 @@ mod tests { generated_to_string(EntityWriter::gen_compact_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &bonus_attributes([r#"serde(rename_all = "camelCase")"#, "ts(export)"]), + &TokenStream::new(), false, + true, )) ); @@ -2038,13 +2661,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -2054,13 +2679,15 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &bonus_attributes([r#"serde(rename_all = "camelCase")"#]), + &TokenStream::new(), false, + true, )) ); assert_eq!( @@ -2070,13 +2697,71 @@ mod tests { generated_to_string(EntityWriter::gen_expanded_code_blocks( &cake_entity, &WithSerde::None, - &DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &bonus_attributes([r#"serde(rename_all = "camelCase")"#, "ts(export)"]), + &TokenStream::new(), false, + true, + )) + ); + + // Frontend code blocks + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_attributes/cake_none.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_attributes/cake_one.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &bonus_attributes([r#"serde(rename_all = "camelCase")"#]), + &TokenStream::new(), + false, + true, + )) + ); + assert_eq!( + comparable_file_string(include_str!( + "../../tests/frontend_with_attributes/cake_multiple.rs" + ))?, + generated_to_string(EntityWriter::gen_frontend_code_blocks( + &cake_entity, + &WithSerde::None, + &default_column_option(), + &None, + false, + false, + &TokenStream::new(), + &bonus_attributes([r#"serde(rename_all = "camelCase")"#, "ts(export)"]), + &TokenStream::new(), + false, + true, )) ); @@ -2127,6 +2812,7 @@ mod tests { auto_increment: true, not_null: true, unique: false, + unique_key: None, }, Column { name: "payload".to_owned(), @@ -2134,6 +2820,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, Column { name: "payload_binary".to_owned(), @@ -2141,6 +2828,7 @@ mod tests { auto_increment: false, not_null: true, unique: false, + unique_key: None, }, ], relations: vec![], @@ -2163,13 +2851,15 @@ mod tests { EntityWriter::gen_compact_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, + &default_column_option(), &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -2180,17 +2870,19 @@ mod tests { .to_string() ); assert_eq!( - parse_from_file(ENTITY_FILES[i].as_bytes())?.to_string(), - EntityWriter::gen_compact_code_blocks( + parse_from_file(ENTITY_FILES_EXPANDED[i].as_bytes())?.to_string(), + EntityWriter::gen_expanded_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, - &Some("public".to_owned()), + &default_column_option(), + &Some("schema_name".to_owned()), false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) @@ -2200,18 +2892,179 @@ mod tests { }) .to_string() ); + } + + Ok(()) + } + + #[test] + fn test_gen_import_active_enum() -> io::Result<()> { + let entities = vec![ + Entity { + table_name: "tea_pairing".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "first_tea".to_owned(), + col_type: ColumnType::Enum { + name: SeaRc::new(Alias::new("tea_enum")), + variants: vec![ + SeaRc::new(Alias::new("everyday_tea")), + SeaRc::new(Alias::new("breakfast_tea")), + ], + }, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "second_tea".to_owned(), + col_type: ColumnType::Enum { + name: SeaRc::new(Alias::new("tea_enum")), + variants: vec![ + SeaRc::new(Alias::new("everyday_tea")), + SeaRc::new(Alias::new("breakfast_tea")), + ], + }, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![], + conjunct_relations: vec![], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "tea_pairing_with_size".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "first_tea".to_owned(), + col_type: ColumnType::Enum { + name: SeaRc::new(Alias::new("tea_enum")), + variants: vec![ + SeaRc::new(Alias::new("everyday_tea")), + SeaRc::new(Alias::new("breakfast_tea")), + ], + }, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "second_tea".to_owned(), + col_type: ColumnType::Enum { + name: SeaRc::new(Alias::new("tea_enum")), + variants: vec![ + SeaRc::new(Alias::new("everyday_tea")), + SeaRc::new(Alias::new("breakfast_tea")), + ], + }, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "size".to_owned(), + col_type: ColumnType::Enum { + name: SeaRc::new(Alias::new("tea_size")), + variants: vec![ + SeaRc::new(Alias::new("small")), + SeaRc::new(Alias::new("medium")), + SeaRc::new(Alias::new("huge")), + ], + }, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![], + conjunct_relations: vec![], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + ]; + + assert_eq!( + quote!( + use super::sea_orm_active_enums::TeaEnum; + ) + .to_string(), + EntityWriter::gen_import_active_enum(&entities[0]).to_string() + ); + + assert_eq!( + quote!( + use super::sea_orm_active_enums::TeaEnum; + use super::sea_orm_active_enums::TeaSize; + ) + .to_string(), + EntityWriter::gen_import_active_enum(&entities[1]).to_string() + ); + + Ok(()) + } + + #[test] + fn test_gen_dense_code_blocks() -> io::Result<()> { + let entities = setup(); + const ENTITY_FILES: [&str; 13] = [ + include_str!("../../tests/dense/cake.rs"), + include_str!("../../tests/dense/cake_filling.rs"), + include_str!("../../tests/dense/cake_filling_price.rs"), + include_str!("../../tests/dense/filling.rs"), + include_str!("../../tests/dense/fruit.rs"), + include_str!("../../tests/dense/vendor.rs"), + include_str!("../../tests/dense/rust_keyword.rs"), + include_str!("../../tests/dense/cake_with_float.rs"), + include_str!("../../tests/dense/cake_with_double.rs"), + include_str!("../../tests/dense/collection.rs"), + include_str!("../../tests/dense/collection_float.rs"), + include_str!("../../tests/dense/parent.rs"), + include_str!("../../tests/dense/child.rs"), + ]; + + assert_eq!(entities.len(), ENTITY_FILES.len()); + + for (i, entity) in entities.iter().enumerate() { assert_eq!( - parse_from_file(ENTITY_FILES_EXPANDED[i].as_bytes())?.to_string(), - EntityWriter::gen_expanded_code_blocks( + parse_from_file(ENTITY_FILES[i].as_bytes())?.to_string(), + EntityWriter::gen_dense_code_blocks( entity, &crate::WithSerde::None, - &crate::DateTimeCrate::Chrono, - &Some("schema_name".to_owned()), + &default_column_option(), + &None, false, false, &TokenStream::new(), &TokenStream::new(), + &TokenStream::new(), false, + true, ) .into_iter() .skip(1) diff --git a/sea-orm-codegen/src/entity/writer/compact.rs b/sea-orm-codegen/src/entity/writer/compact.rs new file mode 100644 index 0000000000..6f9e0c423c --- /dev/null +++ b/sea-orm-codegen/src/entity/writer/compact.rs @@ -0,0 +1,151 @@ +use super::*; + +impl EntityWriter { + #[allow(clippy::too_many_arguments)] + pub fn gen_compact_code_blocks( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + _column_extra_derives: &TokenStream, + seaography: bool, + impl_active_model_behavior: bool, + ) -> Vec { + let mut imports = Self::gen_import(with_serde); + imports.extend(Self::gen_import_active_enum(entity)); + let mut code_blocks = vec![ + imports, + Self::gen_compact_model_struct( + entity, + with_serde, + column_option, + schema_name, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + model_extra_derives, + model_extra_attributes, + ), + Self::gen_compact_relation_enum(entity), + ]; + code_blocks.extend(Self::gen_impl_related(entity)); + code_blocks.extend(Self::gen_impl_conjunct_related(entity)); + if impl_active_model_behavior { + code_blocks.extend([Self::impl_active_model_behavior()]); + } + if seaography { + code_blocks.extend([Self::gen_related_entity(entity)]); + } + code_blocks + } + + #[allow(clippy::too_many_arguments)] + pub fn gen_compact_model_struct( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + ) -> TokenStream { + let table_name = entity.table_name.as_str(); + let column_names_snake_case = entity.get_column_names_snake_case(); + let column_rs_types = entity.get_column_rs_types(column_option); + let if_eq_needed = entity.get_eq_needed(); + let primary_keys: Vec = entity + .primary_keys + .iter() + .map(|pk| pk.name.clone()) + .collect(); + let attrs: Vec = entity + .columns + .iter() + .map(|col| { + let mut attrs: Punctuated<_, Comma> = Punctuated::new(); + let is_primary_key = primary_keys.contains(&col.name); + if !col.is_snake_case_name() { + let column_name = &col.name; + attrs.push(quote! { column_name = #column_name }); + } + if is_primary_key { + attrs.push(quote! { primary_key }); + if !col.auto_increment { + attrs.push(quote! { auto_increment = false }); + } + } + if let Some(ts) = col.get_col_type_attrs() { + attrs.extend([ts]); + if !col.not_null { + attrs.push(quote! { nullable }); + } + }; + if col.unique { + attrs.push(quote! { unique }); + } else if let Some(unique_key) = &col.unique_key { + attrs.push(quote! { unique_key = #unique_key }); + } + let mut ts = quote! {}; + if !attrs.is_empty() { + for (i, attr) in attrs.into_iter().enumerate() { + if i > 0 { + ts = quote! { #ts, }; + } + ts = quote! { #ts #attr }; + } + ts = quote! { #[sea_orm(#ts)] }; + } + let serde_attribute = col.get_serde_attribute( + is_primary_key, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + ); + ts = quote! { + #ts + #serde_attribute + }; + ts + }) + .collect(); + let schema_name = match Self::gen_schema_name(schema_name) { + Some(schema_name) => quote! { + schema_name = #schema_name, + }, + None => quote! {}, + }; + let extra_derive = with_serde.extra_derive(); + + quote! { + #[derive(Clone, Debug, PartialEq #if_eq_needed, DeriveEntityModel #extra_derive #model_extra_derives)] + #[sea_orm( + #schema_name + table_name = #table_name + )] + #model_extra_attributes + pub struct Model { + #( + #attrs + pub #column_names_snake_case: #column_rs_types, + )* + } + } + } + + pub fn gen_compact_relation_enum(entity: &Entity) -> TokenStream { + let attrs = entity.get_relation_attrs(); + let relation_enum_name = entity.get_relation_enum_name(); + quote! { + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation { + #( + #attrs + #relation_enum_name, + )* + } + } + } +} diff --git a/sea-orm-codegen/src/entity/writer/dense.rs b/sea-orm-codegen/src/entity/writer/dense.rs new file mode 100644 index 0000000000..4338071417 --- /dev/null +++ b/sea-orm-codegen/src/entity/writer/dense.rs @@ -0,0 +1,303 @@ +use super::*; +use crate::{Relation, RelationType}; +use heck::ToSnakeCase; + +impl EntityWriter { + #[allow(clippy::too_many_arguments)] + pub fn gen_dense_code_blocks( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + _column_extra_derives: &TokenStream, + _seaography: bool, + impl_active_model_behavior: bool, + ) -> Vec { + let mut imports = Self::gen_import(with_serde); + imports.extend(Self::gen_import_active_enum(entity)); + let mut code_blocks = vec![ + imports, + Self::gen_dense_model_struct( + entity, + with_serde, + column_option, + schema_name, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + model_extra_derives, + model_extra_attributes, + ), + ]; + if impl_active_model_behavior { + code_blocks.push(Self::impl_active_model_behavior()); + } + code_blocks + } + + #[allow(clippy::too_many_arguments)] + pub fn gen_dense_model_struct( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + ) -> TokenStream { + let table_name = entity.table_name.as_str(); + let column_names_snake_case = entity.get_column_names_snake_case(); + let column_rs_types = entity.get_column_rs_types(column_option); + let if_eq_needed = entity.get_eq_needed(); + let primary_keys: Vec = entity + .primary_keys + .iter() + .map(|pk| pk.name.clone()) + .collect(); + let attrs: Vec = entity + .columns + .iter() + .map(|col| { + let mut attrs: Punctuated<_, Comma> = Punctuated::new(); + let is_primary_key = primary_keys.contains(&col.name); + if !col.is_snake_case_name() { + let column_name = &col.name; + attrs.push(quote! { column_name = #column_name }); + } + if is_primary_key { + attrs.push(quote! { primary_key }); + if !col.auto_increment { + attrs.push(quote! { auto_increment = false }); + } + } + if let Some(ts) = col.get_col_type_attrs() { + attrs.extend([ts]); + if !col.not_null { + attrs.push(quote! { nullable }); + } + }; + if col.unique { + attrs.push(quote! { unique }); + } else if let Some(unique_key) = &col.unique_key { + attrs.push(quote! { unique_key = #unique_key }); + } + let mut ts = quote! {}; + if !attrs.is_empty() { + for (i, attr) in attrs.into_iter().enumerate() { + if i > 0 { + ts = quote! { #ts, }; + } + ts = quote! { #ts #attr }; + } + ts = quote! { #[sea_orm(#ts)] }; + } + let serde_attribute = col.get_serde_attribute( + is_primary_key, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + ); + ts = quote! { + #ts + #serde_attribute + }; + ts + }) + .collect(); + let schema_name = match Self::gen_schema_name(schema_name) { + Some(schema_name) => quote! { + schema_name = #schema_name, + }, + None => quote! {}, + }; + let extra_derive = with_serde.extra_derive(); + + let mut compound_objects: Punctuated<_, Comma> = Punctuated::new(); + + let map_col = |a: &syn::Ident| -> String { + let a = a.to_string(); + let b = a.to_snake_case(); + if a != b.to_upper_camel_case() { + // if roundtrip fails, use original + a + } else { + b + } + }; + let map_punctuated = |punctuated: Vec| -> String { + let len = punctuated.len(); + let punctuated = punctuated.join(", "); + match len { + 0..=1 => punctuated, + _ => format!("({punctuated})"), + } + }; + + let via_entities = entity.get_conjunct_relations_via_snake_case(); + for rel in entity.relations.iter() { + if !rel.self_referencing && rel.impl_related { + let (rel_type, sea_orm_attr) = match rel.rel_type { + RelationType::HasOne => (format_ident!("HasOne"), quote!(#[sea_orm(has_one)])), + RelationType::HasMany => { + (format_ident!("HasMany"), quote!(#[sea_orm(has_many)])) + } + RelationType::BelongsTo => { + let (from, to) = rel.get_src_ref_columns(map_col, map_col, map_punctuated); + let on_update = if let Some(action) = &rel.on_update { + let action = Relation::get_foreign_key_action(action); + quote!(, on_update = #action) + } else { + quote!() + }; + let on_delete = if let Some(action) = &rel.on_delete { + let action = Relation::get_foreign_key_action(action); + quote!(, on_delete = #action) + } else { + quote!() + }; + let relation_enum = if rel.num_suffix > 0 { + let relation_enum = rel.get_enum_name().to_string(); + quote!(relation_enum = #relation_enum,) + } else { + quote!() + }; + ( + format_ident!("HasOne"), + quote!(#[sea_orm(belongs_to, #relation_enum from = #from, to = #to #on_update #on_delete)]), + ) + } + }; + + if let Some(to_entity) = rel.get_module_name() { + if !via_entities.contains(&to_entity) { + // skip junctions + let field = if matches!(rel.rel_type, RelationType::HasMany) { + format_ident!( + "{}", + pluralizer::pluralize(&to_entity.to_string(), 2, false) + ) + } else { + to_entity.clone() + }; + let field = if rel.num_suffix == 0 { + field + } else { + format_ident!("{field}_{}", rel.num_suffix) + }; + compound_objects.push(quote! { + #sea_orm_attr + pub #field: #rel_type + }); + } + } + } else if rel.self_referencing { + let (from, to) = rel.get_src_ref_columns(map_col, map_col, map_punctuated); + let on_update = if let Some(action) = &rel.on_update { + let action = Relation::get_foreign_key_action(action); + quote!(, on_update = #action) + } else { + quote!() + }; + let on_delete = if let Some(action) = &rel.on_delete { + let action = Relation::get_foreign_key_action(action); + quote!(, on_delete = #action) + } else { + quote!() + }; + let relation_enum = rel.get_enum_name().to_string(); + let field = format_ident!( + "{}{}", + entity.get_table_name_snake_case_ident(), + if rel.num_suffix > 0 { + format!("_{}", rel.num_suffix) + } else { + "".into() + } + ); + + compound_objects.push(quote! { + #[sea_orm(self_ref, relation_enum = #relation_enum, from = #from, to = #to #on_update #on_delete)] + pub #field: HasOne + }); + } + } + for (to_entity, via_entity) in entity + .get_conjunct_relations_to_snake_case() + .into_iter() + .zip(via_entities) + { + let field = format_ident!( + "{}", + pluralizer::pluralize(&to_entity.to_string(), 2, false) + ); + let via_entity = via_entity.to_string(); + compound_objects.push(quote! { + #[sea_orm(has_many, via = #via_entity)] + pub #field: HasMany + }); + } + + if !compound_objects.is_empty() { + compound_objects.push_punct(::default()); + } + + quote! { + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq #if_eq_needed, DeriveEntityModel #extra_derive #model_extra_derives)] + #[sea_orm( + #schema_name + table_name = #table_name + )] + #model_extra_attributes + pub struct Model { + #( + #attrs + pub #column_names_snake_case: #column_rs_types, + )* + #compound_objects + } + } + } + + #[allow(dead_code)] + fn gen_dense_related_entity(entity: &Entity) -> TokenStream { + let via_entities = entity.get_conjunct_relations_via_snake_case(); + + let related_modules = entity.get_related_entity_modules(); + let related_attrs = entity.get_related_entity_attrs(); + let related_enum_names = entity.get_related_entity_enum_name(); + + let items = related_modules + .into_iter() + .zip(related_attrs) + .zip(related_enum_names) + .filter_map(|((related_module, related_attr), related_enum_name)| { + if !via_entities.contains(&related_module) { + // skip junctions + Some(quote!(#related_attr #related_enum_name)) + } else { + None + } + }) + .collect::>(); + + quote! { + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] + pub enum RelatedEntity { + #(#items),* + } + } + } +} + +#[cfg(test)] +mod test { + #[test] + #[ignore] + fn test_name() { + panic!("{}", pluralizer::pluralize("filling", 2, false)); + } +} diff --git a/sea-orm-codegen/src/entity/writer/expanded.rs b/sea-orm-codegen/src/entity/writer/expanded.rs new file mode 100644 index 0000000000..76b81ee843 --- /dev/null +++ b/sea-orm-codegen/src/entity/writer/expanded.rs @@ -0,0 +1,80 @@ +use super::*; + +impl EntityWriter { + #[allow(clippy::too_many_arguments)] + pub fn gen_expanded_code_blocks( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + column_extra_derives: &TokenStream, + seaography: bool, + impl_active_model_behavior: bool, + ) -> Vec { + let mut imports = Self::gen_import(with_serde); + imports.extend(Self::gen_import_active_enum(entity)); + let mut code_blocks = vec![ + imports, + Self::gen_entity_struct(), + Self::gen_impl_entity_name(entity, schema_name), + Self::gen_expanded_model_struct( + entity, + with_serde, + column_option, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + model_extra_derives, + model_extra_attributes, + ), + Self::gen_column_enum(entity, column_extra_derives), + Self::gen_primary_key_enum(entity), + Self::gen_impl_primary_key(entity, column_option), + Self::gen_relation_enum(entity), + Self::gen_impl_column_trait(entity), + Self::gen_impl_relation_trait(entity), + ]; + code_blocks.extend(Self::gen_impl_related(entity)); + code_blocks.extend(Self::gen_impl_conjunct_related(entity)); + if impl_active_model_behavior { + code_blocks.extend([Self::impl_active_model_behavior()]); + } + if seaography { + code_blocks.extend([Self::gen_related_entity(entity)]); + } + code_blocks + } + + pub fn gen_expanded_model_struct( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + ) -> TokenStream { + let column_names_snake_case = entity.get_column_names_snake_case(); + let column_rs_types = entity.get_column_rs_types(column_option); + let if_eq_needed = entity.get_eq_needed(); + let serde_attributes = entity.get_column_serde_attributes( + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + ); + let extra_derive = with_serde.extra_derive(); + + quote! { + #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel #if_eq_needed #extra_derive #model_extra_derives)] + #model_extra_attributes + pub struct Model { + #( + #serde_attributes + pub #column_names_snake_case: #column_rs_types, + )* + } + } + } +} diff --git a/sea-orm-codegen/src/entity/writer/frontend.rs b/sea-orm-codegen/src/entity/writer/frontend.rs new file mode 100644 index 0000000000..6b2fa2507c --- /dev/null +++ b/sea-orm-codegen/src/entity/writer/frontend.rs @@ -0,0 +1,80 @@ +use super::*; + +impl EntityWriter { + #[allow(clippy::too_many_arguments)] + pub fn gen_frontend_code_blocks( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + _column_extra_derives: &TokenStream, + _seaography: bool, + _impl_active_model_behavior: bool, + ) -> Vec { + let mut imports = Self::gen_import_serde(with_serde); + imports.extend(Self::gen_import_active_enum(entity)); + let code_blocks = vec![ + imports, + Self::gen_frontend_model_struct( + entity, + with_serde, + column_option, + schema_name, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + model_extra_derives, + model_extra_attributes, + ), + ]; + code_blocks + } + + #[allow(clippy::too_many_arguments)] + pub fn gen_frontend_model_struct( + entity: &Entity, + with_serde: &WithSerde, + column_option: &ColumnOption, + _schema_name: &Option, + serde_skip_deserializing_primary_key: bool, + serde_skip_hidden_column: bool, + model_extra_derives: &TokenStream, + model_extra_attributes: &TokenStream, + ) -> TokenStream { + let column_names_snake_case = entity.get_column_names_snake_case(); + let column_rs_types = entity.get_column_rs_types(column_option); + let if_eq_needed = entity.get_eq_needed(); + let primary_keys: Vec = entity + .primary_keys + .iter() + .map(|pk| pk.name.clone()) + .collect(); + let attrs: Vec = entity + .columns + .iter() + .map(|col| { + let is_primary_key = primary_keys.contains(&col.name); + col.get_serde_attribute( + is_primary_key, + serde_skip_deserializing_primary_key, + serde_skip_hidden_column, + ) + }) + .collect(); + let extra_derive = with_serde.extra_derive(); + + quote! { + #[derive(Clone, Debug, PartialEq #if_eq_needed #extra_derive #model_extra_derives)] + #model_extra_attributes + pub struct Model { + #( + #attrs + pub #column_names_snake_case: #column_rs_types, + )* + } + } + } +} diff --git a/sea-orm-codegen/src/entity/writer/mermaid.rs b/sea-orm-codegen/src/entity/writer/mermaid.rs new file mode 100644 index 0000000000..5793ed02db --- /dev/null +++ b/sea-orm-codegen/src/entity/writer/mermaid.rs @@ -0,0 +1,404 @@ +use std::collections::{BTreeSet, HashSet}; +use std::fmt::Write; + +use sea_query::ColumnType; + +use crate::{Entity, RelationType}; + +use super::EntityWriter; + +impl EntityWriter { + pub fn generate_er_diagram(&self) -> String { + let mut out = String::from("erDiagram\n"); + + let pk_sets: Vec> = self + .entities + .iter() + .map(|e| e.primary_keys.iter().map(|pk| pk.name.as_str()).collect()) + .collect(); + + let fk_sets: Vec> = self + .entities + .iter() + .map(|e| { + e.relations + .iter() + .filter(|r| matches!(r.rel_type, RelationType::BelongsTo)) + .flat_map(|r| r.columns.iter().map(String::as_str)) + .collect() + }) + .collect(); + + for (i, entity) in self.entities.iter().enumerate() { + write_entity_block(&mut out, entity, &pk_sets[i], &fk_sets[i]); + } + + let mut emitted: BTreeSet = BTreeSet::new(); + + for entity in &self.entities { + write_relations(&mut out, entity, &mut emitted); + } + + out + } +} + +fn write_entity_block(out: &mut String, entity: &Entity, pks: &HashSet<&str>, fks: &HashSet<&str>) { + let _ = writeln!(out, " {} {{", entity.table_name); + + for col in &entity.columns { + let type_name = col_type_name(&col.col_type); + let is_pk = pks.contains(col.name.as_str()); + let is_fk = fks.contains(col.name.as_str()); + let is_uk = col.unique || col.unique_key.is_some(); + + let constraint = match (is_pk, is_fk, is_uk) { + (true, true, _) => " PK,FK", + (true, false, _) => " PK", + (false, true, true) => " FK,UK", + (false, true, false) => " FK", + (false, false, true) => " UK", + (false, false, false) => "", + }; + + let _ = writeln!(out, " {} {}{}", type_name, col.name, constraint); + } + + let _ = writeln!(out, " }}"); +} + +fn write_relations(out: &mut String, entity: &Entity, emitted: &mut BTreeSet) { + for rel in &entity.relations { + let (left, right, cardinality, label) = match rel.rel_type { + RelationType::BelongsTo => ( + &entity.table_name, + &rel.ref_table, + "}o--||", + rel.columns.join(", "), + ), + RelationType::HasOne => continue, + RelationType::HasMany => continue, + }; + + let key = format!("{left} {cardinality} {right} : \"{label}\""); + if emitted.insert(key.clone()) { + let _ = writeln!(out, " {key}"); + } + } + + for conj in &entity.conjunct_relations { + let left = &entity.table_name; + let right = &conj.to; + let label = format!("[{}]", conj.via); + + let key = if left <= right { + format!("{left} }}o--o{{ {right} : \"{label}\"") + } else { + format!("{right} }}o--o{{ {left} : \"{label}\"") + }; + + if emitted.insert(key.clone()) { + let _ = writeln!(out, " {key}"); + } + } +} + +fn col_type_name(col_type: &ColumnType) -> &str { + #[allow(unreachable_patterns)] + match col_type { + ColumnType::Char(_) => "char", + ColumnType::String(_) => "varchar", + ColumnType::Text => "text", + ColumnType::TinyInteger => "tinyint", + ColumnType::SmallInteger => "smallint", + ColumnType::Integer => "int", + ColumnType::BigInteger => "bigint", + ColumnType::TinyUnsigned => "tinyint_unsigned", + ColumnType::SmallUnsigned => "smallint_unsigned", + ColumnType::Unsigned => "int_unsigned", + ColumnType::BigUnsigned => "bigint_unsigned", + ColumnType::Float => "float", + ColumnType::Double => "double", + ColumnType::Decimal(_) => "decimal", + ColumnType::Money(_) => "money", + ColumnType::DateTime => "datetime", + ColumnType::Timestamp => "timestamp", + ColumnType::TimestampWithTimeZone => "timestamptz", + ColumnType::Time => "time", + ColumnType::Date => "date", + ColumnType::Year => "year", + ColumnType::Binary(_) | ColumnType::VarBinary(_) | ColumnType::Blob => "blob", + ColumnType::Boolean => "bool", + ColumnType::Json | ColumnType::JsonBinary => "json", + ColumnType::Uuid => "uuid", + ColumnType::Enum { .. } => "enum", + ColumnType::Array(_) => "array", + ColumnType::Vector(_) => "vector", + ColumnType::Bit(_) | ColumnType::VarBit(_) => "bit", + ColumnType::Cidr => "cidr", + ColumnType::Inet => "inet", + ColumnType::MacAddr => "macaddr", + ColumnType::LTree => "ltree", + ColumnType::Interval(_, _) => "interval", + ColumnType::Custom(_) => "custom", + _ => "unknown", + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use sea_query::{ColumnType, StringLen}; + + use crate::{ + Column, ConjunctRelation, Entity, EntityWriter, PrimaryKey, Relation, RelationType, + }; + + fn setup_blog_schema() -> EntityWriter { + EntityWriter { + entities: vec![ + Entity { + table_name: "user".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(StringLen::N(255)), + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "email".to_owned(), + col_type: ColumnType::String(StringLen::N(255)), + auto_increment: false, + not_null: true, + unique: true, + unique_key: None, + }, + Column { + name: "parent_id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: false, + unique: false, + unique_key: None, + }, + ], + relations: vec![ + Relation { + ref_table: "post".to_owned(), + columns: vec![], + ref_columns: vec![], + rel_type: RelationType::HasMany, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }, + Relation { + ref_table: "user".to_owned(), + columns: vec!["parent_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + on_delete: None, + on_update: None, + self_referencing: true, + num_suffix: 0, + impl_related: true, + }, + ], + conjunct_relations: vec![], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "post".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "title".to_owned(), + col_type: ColumnType::Text, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "user_id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![Relation { + ref_table: "user".to_owned(), + columns: vec!["user_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }], + conjunct_relations: vec![ConjunctRelation { + via: "post_tag".to_owned(), + to: "tag".to_owned(), + }], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "tag".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(StringLen::N(100)), + auto_increment: false, + not_null: true, + unique: true, + unique_key: None, + }, + ], + relations: vec![], + conjunct_relations: vec![ConjunctRelation { + via: "post_tag".to_owned(), + to: "post".to_owned(), + }], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "post_tag".to_owned(), + columns: vec![ + Column { + name: "post_id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + Column { + name: "tag_id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: false, + not_null: true, + unique: false, + unique_key: None, + }, + ], + relations: vec![ + Relation { + ref_table: "post".to_owned(), + columns: vec!["post_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }, + Relation { + ref_table: "tag".to_owned(), + columns: vec!["tag_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + on_delete: None, + on_update: None, + self_referencing: false, + num_suffix: 0, + impl_related: true, + }, + ], + conjunct_relations: vec![], + primary_keys: vec![ + PrimaryKey { + name: "post_id".to_owned(), + }, + PrimaryKey { + name: "tag_id".to_owned(), + }, + ], + }, + ], + enums: BTreeMap::new(), + } + } + + #[test] + fn test_generate_er_diagram() { + let writer = setup_blog_schema(); + let diagram = writer.generate_er_diagram(); + + let expected = r#"erDiagram + user { + int id PK + varchar name + varchar email UK + int parent_id FK + } + post { + int id PK + text title + int user_id FK + } + tag { + int id PK + varchar name UK + } + post_tag { + int post_id PK,FK + int tag_id PK,FK + } + user }o--|| user : "parent_id" + post }o--|| user : "user_id" + post }o--o{ tag : "[post_tag]" + post_tag }o--|| post : "post_id" + post_tag }o--|| tag : "tag_id" +"#; + + assert_eq!(diagram, expected); + } + + #[test] + fn test_er_diagram_deduplicates_m2m() { + let writer = setup_blog_schema(); + let diagram = writer.generate_er_diagram(); + + let m2m_count = diagram.matches("}o--o{").count(); + assert_eq!(m2m_count, 1, "M-N relation should appear only once"); + } +} diff --git a/sea-orm-codegen/src/lib.rs b/sea-orm-codegen/src/lib.rs index 1cd62ade49..d004d14f2b 100644 --- a/sea-orm-codegen/src/lib.rs +++ b/sea-orm-codegen/src/lib.rs @@ -1,9 +1,11 @@ mod entity; mod error; +mod merge; mod util; pub use entity::*; pub use error::*; +pub use merge::*; #[cfg(test)] mod tests_cfg; diff --git a/sea-orm-codegen/src/merge/extract.rs b/sea-orm-codegen/src/merge/extract.rs new file mode 100644 index 0000000000..d99082f17d --- /dev/null +++ b/sea-orm-codegen/src/merge/extract.rs @@ -0,0 +1,43 @@ +use super::is_active_model_behavior_impl; +use syn::File; +use syn::Item; +use syn::ItemEnum; +use syn::ItemImpl; +use syn::ItemStruct; +use syn::ItemUse; + +pub(super) fn extract_active_model_behavior_impls(file: &File) -> impl Iterator { + file.items.iter().filter_map(|item| match item { + Item::Impl(item_impl) if is_active_model_behavior_impl(item_impl) => Some(item_impl), + _ => None, + }) +} + +pub(super) fn extract_top_level_uses(file: &File) -> impl Iterator { + file.items.iter().filter_map(|item| match item { + Item::Use(item_use) => Some(item_use), + _ => None, + }) +} + +pub(super) fn find_model_struct(file: &File) -> Option<&ItemStruct> { + file.items.iter().find_map(|item| { + if let Item::Struct(item_struct) = item { + if item_struct.ident == "Model" { + return Some(item_struct); + } + } + None + }) +} + +pub(super) fn find_relation_enum(file: &File) -> Option<&ItemEnum> { + file.items.iter().find_map(|item| { + if let Item::Enum(item_enum) = item { + if item_enum.ident == "Relation" { + return Some(item_enum); + } + } + None + }) +} diff --git a/sea-orm-codegen/src/merge/mod.rs b/sea-orm-codegen/src/merge/mod.rs new file mode 100644 index 0000000000..8462e41705 --- /dev/null +++ b/sea-orm-codegen/src/merge/mod.rs @@ -0,0 +1,795 @@ +use std::collections::{HashMap, HashSet}; + +use prettyplease::unparse; +use syn::fold::{self, Fold}; +use syn::{ + Attribute, Fields, Ident, Item, ItemImpl, Path, parse::Parser, parse_file, + punctuated::Punctuated, +}; +use syn::{ItemUse, Meta}; + +mod extract; +use extract::*; + +#[derive(Default)] +struct OldIndex<'a> { + model_attrs: &'a [Attribute], + model_field_attrs: HashMap<&'a Ident, &'a [Attribute]>, + + relation_attrs: &'a [Attribute], + relation_variant_attrs: HashMap<&'a Ident, &'a [Attribute]>, +} + +impl<'a> OldIndex<'a> { + fn from_file(file: &'a syn::File) -> OldIndex<'a> { + let mut idx = OldIndex::default(); + + if let Some(model) = find_model_struct(file) { + idx.model_attrs = &model.attrs; + if let Fields::Named(named) = &model.fields { + for field in &named.named { + if let Some(ident) = &field.ident { + idx.model_field_attrs.insert(ident, &field.attrs); + } + } + } + } + + if let Some(relation) = find_relation_enum(file) { + idx.relation_attrs = &relation.attrs; + for variant in &relation.variants { + idx.relation_variant_attrs + .insert(&variant.ident, &variant.attrs); + } + } + + idx + } +} + +struct Merger<'a> { + old: OldIndex<'a>, + seen_use: HashSet, + extra_uses: Vec<&'a syn::ItemUse>, + old_behaviors: Vec<&'a syn::ItemImpl>, +} + +impl<'a> Merger<'a> { + fn new( + old: OldIndex<'a>, + extra_uses: Vec<&'a syn::ItemUse>, + old_behaviors: Vec<&'a syn::ItemImpl>, + ) -> Self { + Self { + old, + seen_use: HashSet::new(), + extra_uses, + old_behaviors, + } + } +} + +impl<'a> Fold for Merger<'a> { + fn fold_item_struct(&mut self, i: syn::ItemStruct) -> syn::ItemStruct { + let mut i = fold::fold_item_struct(self, i); + if i.ident == "Model" { + merge_item_attributes(&mut i.attrs, self.old.model_attrs); + if let Fields::Named(named) = &mut i.fields { + for field in &mut named.named { + if let Some(ident) = &field.ident { + if let Some(old_attrs) = self.old.model_field_attrs.get(ident) { + merge_item_attributes(&mut field.attrs, old_attrs); + } + } + } + } + } + i + } + + fn fold_item_enum(&mut self, i: syn::ItemEnum) -> syn::ItemEnum { + let mut i = fold::fold_item_enum(self, i); + if i.ident == "Relation" { + merge_item_attributes(&mut i.attrs, self.old.relation_attrs); + for variant in &mut i.variants { + if let Some(old_attrs) = self.old.relation_variant_attrs.get(&variant.ident) { + merge_item_attributes(&mut variant.attrs, old_attrs); + } + } + } + i + } + + fn fold_file(&mut self, mut file: syn::File) -> syn::File { + let mut uses: Vec = Vec::new(); + let mut others: Vec = Vec::new(); + + for item in file.items { + match item { + Item::Use(u) => { + let u = fold::fold_item_use(self, u); + if self.seen_use.insert(u.clone()) { + uses.push(Item::Use(u)); + } + } + Item::Impl(i) => { + if is_active_model_behavior_impl(&i) { + continue; + } + let i = fold::fold_item_impl(self, i); + others.push(Item::Impl(i)); + } + other => { + let other = fold::fold_item(self, other); + others.push(other); + } + } + } + + for &u in &self.extra_uses { + if self.seen_use.insert(u.clone()) { + uses.push(Item::Use(u.clone())); + } + } + + for &i in &self.old_behaviors { + others.push(Item::Impl(i.clone())); + } + + file.items = { + let mut v = Vec::with_capacity(uses.len() + others.len()); + v.extend(uses); + v.extend(others); + v + }; + file + } +} + +#[derive(Debug, Default)] +#[doc(hidden)] +pub struct MergeReport { + pub output: String, + pub warnings: Vec, + pub fallback_applied: bool, +} + +impl MergeReport { + fn fallback(old_src: &str, new_src: &str) -> Self { + let mut warnings = Vec::new(); + let mut appended = String::from(new_src); + + warnings.push( + "Parsing failed. The previous file content has been preserved as comments at the end." + .to_owned(), + ); + + if !appended.ends_with("\n\n") { + appended.push('\n'); + } + + appended.push_str( + "// --- Preserved original file content (could not be merged automatically) ---\n", + ); + appended.push_str("/*\n"); + appended.push_str(old_src); + if !old_src.ends_with('\n') { + appended.push('\n'); + } + appended.push_str("*/\n\n"); + + MergeReport { + output: appended, + warnings, + fallback_applied: true, + } + } +} + +#[doc(hidden)] +pub fn merge_entity_files(old_src: &str, new_src: &str) -> Result { + let new_file = match parse_file(new_src) { + Ok(file) => file, + Err(err) => { + let mut report = MergeReport::fallback(old_src, new_src); + report.warnings.push(format!( + "Unable to parse new file, generated fallback: {err}" + )); + return Err(report); + } + }; + + let old_file = match parse_file(old_src) { + Ok(file) => file, + Err(err) => { + let mut report = MergeReport::fallback(old_src, new_src); + report.warnings.push(format!( + "Unable to parse old file, generated fallback: {err}" + )); + return Err(report); + } + }; + + let old_index = OldIndex::from_file(&old_file); + let extra_uses = extract_top_level_uses(&old_file).collect::>(); + let old_behaviors = extract_active_model_behavior_impls(&old_file).collect::>(); + let mut folder = Merger::new(old_index, extra_uses, old_behaviors); + let merged_file = folder.fold_file(new_file); + + let output = render_file_with_spacing(merged_file); + Ok(output) +} + +fn merge_item_attributes(new_attrs: &mut Vec, old_attrs: &[Attribute]) { + merge_derives(new_attrs, old_attrs); + + let mut path_idx_map = HashMap::::new(); + let mut doc_attr_set = HashSet::::new(); + + for (idx, attr) in new_attrs.iter().enumerate() { + if is_doc_attribute(attr) { + doc_attr_set.insert(attr.clone()); + continue; + } + if attr.path().is_ident("derive") { + continue; + } + path_idx_map.entry(attr.path().clone()).or_insert(idx); + } + + for old_attr in old_attrs { + if old_attr.path().is_ident("derive") { + continue; + } + + if is_doc_attribute(old_attr) { + if !doc_attr_set.contains(old_attr) { + doc_attr_set.insert(old_attr.clone()); + new_attrs.push(old_attr.clone()); + } + continue; + } + + if let Some(&idx) = path_idx_map.get(old_attr.path()) { + let current = &new_attrs[idx]; + if let Some(merged) = merge_non_derive_attribute(old_attr, current) { + new_attrs[idx] = merged; + } + } else { + path_idx_map.insert(old_attr.path().clone(), new_attrs.len()); + new_attrs.push(old_attr.clone()); + } + } +} + +fn merge_derives(new_attrs: &mut Vec, old_attrs: &[Attribute]) { + let mut derive_paths: Vec = Vec::new(); + let mut seen = HashSet::new(); + let mut insert_index = None; + let mut idx = 0usize; + while idx < new_attrs.len() { + if new_attrs[idx].path().is_ident("derive") { + insert_index.get_or_insert(idx); + if let Some(paths) = parse_derive_paths(&new_attrs[idx]) { + for path in paths { + if seen.insert(path.clone()) { + derive_paths.push(path); + } + } + } + new_attrs.remove(idx); + } else { + idx += 1; + } + } + + for attr in old_attrs + .iter() + .filter(|attr| attr.path().is_ident("derive")) + { + if let Some(paths) = parse_derive_paths(attr) { + for path in paths { + if seen.insert(path.clone()) { + derive_paths.push(path); + } + } + } + } + + if derive_paths.is_empty() { + return; + } + + if insert_index.is_none() { + insert_index = Some( + new_attrs + .iter() + .position(|attr| !is_doc_attribute(attr)) + .unwrap_or(0), + ); + } + + let derive_attr: Attribute = syn::parse_quote!(#[derive(#(#derive_paths),*)]); + new_attrs.insert(insert_index.unwrap_or(0), derive_attr); +} + +fn parse_derive_paths(attr: &Attribute) -> Option> { + attr.parse_args_with(Punctuated::::parse_terminated) + .ok() + .map(|punct| punct.into_iter().collect()) +} + +fn merge_non_derive_attribute(old_attr: &Attribute, new_attr: &Attribute) -> Option { + use syn::{Token, punctuated::Punctuated}; + + let parse_attr = |attr: &Attribute| { + attr.parse_args_with(Punctuated::::parse_terminated) + .ok() + }; + + let old_args = parse_attr(old_attr)?; + let new_args = parse_attr(new_attr)?; + + fn meta_key(meta: &Meta) -> &Path { + match meta { + Meta::Path(p) => p, + Meta::NameValue(nv) => &nv.path, + Meta::List(list) => &list.path, + } + } + + let mut seen_keys = HashSet::::new(); + let mut merged: Vec = Vec::new(); + + for m in old_args.into_iter() { + // Always preserve old attribute values on key conflict + seen_keys.insert(meta_key(&m).clone()); + merged.push(m); + } + + for m in new_args.into_iter() { + let key = meta_key(&m); + if !seen_keys.contains(key) { + seen_keys.insert(key.clone()); + merged.push(m); + } + } + + let path = old_attr.path(); + let tokens = quote::quote!(#[#path(#(#merged),*)]); + Attribute::parse_outer + .parse2(tokens) + .ok() + .and_then(|v| v.into_iter().next()) +} + +fn is_active_model_behavior_impl(item_impl: &ItemImpl) -> bool { + let trait_ident = item_impl + .trait_ + .as_ref() + .and_then(|(_, path, _)| path.segments.last()) + .map(|segment| &segment.ident); + let self_ident = match item_impl.self_ty.as_ref() { + syn::Type::Path(type_path) => type_path.path.segments.last().map(|seg| &seg.ident), + _ => None, + }; + matches!(trait_ident, Some(ident) if ident == "ActiveModelBehavior") + && matches!(self_ident, Some(ident) if ident == "ActiveModel") +} + +fn is_doc_attribute(attr: &Attribute) -> bool { + attr.path().is_ident("doc") +} + +fn render_file_with_spacing(file: syn::File) -> String { + fn trim_trailing_newlines(s: &str) -> &str { + s.trim_end_matches(['\n', '\r']) + } + + fn render_items_with_sep(items: &[Item], separator: &str) -> Option { + let mut rendered_parts = Vec::with_capacity(items.len()); + for item in items { + let single_item_file = syn::File { + shebang: None, + attrs: Vec::new(), + items: vec![item.clone()], + }; + let rendered = unparse(&single_item_file); + let trimmed = trim_trailing_newlines(&rendered); + if !trimmed.is_empty() { + rendered_parts.push(trimmed.to_owned()); + } + } + if rendered_parts.is_empty() { + None + } else { + Some(rendered_parts.join(separator)) + } + } + + let syn::File { + shebang, + attrs, + items, + } = file; + + let (use_items, other_items): (Vec, Vec) = + items.into_iter().partition(|it| matches!(it, Item::Use(_))); + + let mut out = String::new(); + + if let Some(s) = shebang { + out.push_str(s.trim_end_matches('\n')); + out.push('\n'); + } + + if !attrs.is_empty() { + if !out.is_empty() && !out.ends_with('\n') { + out.push('\n'); + } + let attrs_file = syn::File { + shebang: None, + attrs, + items: vec![], + }; + let rendered = unparse(&attrs_file); + let trimmed = trim_trailing_newlines(&rendered); + if !trimmed.is_empty() { + out.push_str(trimmed); + out.push('\n'); + out.push('\n'); + } + } + + if let Some(block) = render_items_with_sep(&use_items, "\n") { + if !out.is_empty() && !out.ends_with('\n') { + out.push('\n'); + } + if !block.is_empty() { + out.push_str(&block); + out.push('\n'); + } + } + + if let Some(block) = render_items_with_sep(&other_items, "\n\n") { + if !out.is_empty() && !out.ends_with("\n\n") { + if out.ends_with('\n') { + out.push('\n'); + } else { + out.push_str("\n\n"); + } + } + out.push_str(&block); + out.push('\n'); + } + + if !out.ends_with('\n') { + out.push('\n'); + } + + out +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + + use super::*; + + #[test] + fn merge_preserves_behavior_and_attributes() { + let old_file = indoc! {r#" + use sea_orm::entity::prelude::*; + use crate::helpers::Helper; + + #[derive(Clone, Debug, DeriveEntityModel, serde::Serialize)] + #[sea_orm(table_name = "posts")] + #[serde(rename_all = "camelCase")] + /// Old model docs + pub struct Model { + /// Old id docs + #[serde(rename = "postId")] + #[cfg(feature = "serde")] + pub id: i32, + pub title: String, + } + + #[derive(DeriveRelation, Eq, PartialEq)] + #[ts(export)] + pub enum Relation { + #[sea_orm(has_one = "super::super::MyEntity")] + MyRelation, + } + + impl ActiveModelBehavior for ActiveModel { + fn custom(&self) -> Helper { + Helper::default() + } + } + "# + }; + + let curr = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(Clone, Debug, DeriveEntityModel)] + #[sea_orm(schema_name = "public")] + pub struct Model { + pub id: i32, + pub title: String, + } + + #[derive(DeriveRelation)] + pub enum Relation { + MyRelation, + } + + impl ActiveModelBehavior for ActiveModel {} + "# + }; + + let merged = merge_entity_files(old_file, curr).expect("merge failed"); + let expected = indoc! {r#" + use sea_orm::entity::prelude::*; + use crate::helpers::Helper; + + #[derive(Clone, Debug, DeriveEntityModel, serde::Serialize)] + #[sea_orm(table_name = "posts", schema_name = "public")] + #[serde(rename_all = "camelCase")] + /// Old model docs + pub struct Model { + /// Old id docs + #[serde(rename = "postId")] + #[cfg(feature = "serde")] + pub id: i32, + pub title: String, + } + + #[derive(DeriveRelation, Eq, PartialEq)] + #[ts(export)] + pub enum Relation { + #[sea_orm(has_one = "super::super::MyEntity")] + MyRelation, + } + + impl ActiveModelBehavior for ActiveModel { + fn custom(&self) -> Helper { + Helper::default() + } + } + "# + }; + + assert_eq!(merged, expected); + } + + #[test] + fn merge_handles_field_changes() { + let old_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + #[serde(default)] + pub id: i32, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "foo")] + pub removed: bool, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + let new_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + pub id: i32, + pub name: String, + #[serde(rename = "bar")] + pub added: String, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + let merged = merge_entity_files(old_src, new_src).expect("merge should succeed"); + + let expected = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + #[serde(default)] + pub id: i32, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "bar")] + pub added: String, + } + + impl ActiveModelBehavior for ActiveModel {} + "# + }; + + assert_eq!(merged, expected); + } + + #[test] + fn complex_use() { + let old_src = indoc! {r#" + use crate::{ + B::{self, C}, + A, + D as E, + }; + use self::{ + helper::Tool as ToolAlias, + super::shared::Common, + }; + + pub struct Placeholder; + "# + }; + + let new_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + pub struct Placeholder; + "# + }; + + let merged = merge_entity_files(old_src, new_src).expect("merge should succeed"); + + let expected = indoc! {r#" + use sea_orm::entity::prelude::*; + use crate::{ + B::{self, C}, + A, D as E, + }; + use self::{helper::Tool as ToolAlias, super::shared::Common}; + + pub struct Placeholder; + "# + }; + + assert_eq!(merged, expected); + } + + #[test] + fn conv_to_comment_fallback() { + let old_src = indoc! {r#" + this is not valid rust + impl ActiveModelBehavior for ActiveModel { + fn something(&self) {} + } + "# + }; + + let new_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "# + }; + + let report = merge_entity_files(old_src, new_src).unwrap_err(); + assert!(report.fallback_applied); + + let expect = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + + // --- Preserved original file content (could not be merged automatically) --- + /* + this is not valid rust + impl ActiveModelBehavior for ActiveModel { + fn something(&self) {} + } + */ + + "#}; + + assert_eq!(report.output, expect) + } + + #[test] + fn conflict_attrs_should_never_be_overwritten() { + let old_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + #[serde(rename_all = "camelCase")] + pub struct Model { + #[serde(rename_all = "foo")] + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + let new_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + #[serde(rename_all = "snake_case")] + pub struct Model { + #[serde(rename_all = "bar")] + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + let merged = merge_entity_files(old_src, new_src).expect("merge should succeed"); + let expected = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + #[serde(rename_all = "camelCase")] + pub struct Model { + #[serde(rename_all = "foo")] + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + assert_eq!(merged, expected); + } + + #[test] + fn conflict_attrs_should_be_merged() { + let old_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + #[serde(rename = "oldId")] + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + let new_src = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + #[serde(rename = "newId", default)] + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + let merged = merge_entity_files(old_src, new_src).expect("merge should succeed"); + let expected = indoc! {r#" + use sea_orm::entity::prelude::*; + + #[derive(DeriveEntityModel)] + pub struct Model { + #[serde(rename = "oldId", default)] + pub id: i32, + } + + impl ActiveModelBehavior for ActiveModel {} + "#}; + + assert_eq!(merged, expected); + } +} diff --git a/sea-orm-codegen/src/tests_cfg/compact/indexes.rs b/sea-orm-codegen/src/tests_cfg/compact/indexes.rs new file mode 100644 index 0000000000..bedcd098ca --- /dev/null +++ b/sea-orm-codegen/src/tests_cfg/compact/indexes.rs @@ -0,0 +1,20 @@ +//! An entity definition for testing table index creation. +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "indexes")] +pub struct Model { + #[sea_orm(primary_key)] + pub indexes_id: i32, + #[sea_orm(unique)] + pub unique_attr: i32, + #[sea_orm(unique_key = "my_unique")] + pub unique_key_a: String, + #[sea_orm(unique_key = "my_unique")] + pub unique_key_b: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/src/tests_cfg/compact/mod.rs b/sea-orm-codegen/src/tests_cfg/compact/mod.rs new file mode 100644 index 0000000000..cd58b1fc6f --- /dev/null +++ b/sea-orm-codegen/src/tests_cfg/compact/mod.rs @@ -0,0 +1 @@ +pub mod indexes; diff --git a/sea-orm-codegen/src/tests_cfg/dense/indexes.rs b/sea-orm-codegen/src/tests_cfg/dense/indexes.rs new file mode 100644 index 0000000000..f50d0702b0 --- /dev/null +++ b/sea-orm-codegen/src/tests_cfg/dense/indexes.rs @@ -0,0 +1,18 @@ +//! An entity definition for testing table index creation. +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "indexes")] +pub struct Model { + #[sea_orm(primary_key)] + pub indexes_id: i32, + #[sea_orm(unique)] + pub unique_attr: i32, + #[sea_orm(unique_key = "my_unique")] + pub unique_key_a: String, + #[sea_orm(unique_key = "my_unique")] + pub unique_key_b: String, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/src/tests_cfg/dense/mod.rs b/sea-orm-codegen/src/tests_cfg/dense/mod.rs new file mode 100644 index 0000000000..cd58b1fc6f --- /dev/null +++ b/sea-orm-codegen/src/tests_cfg/dense/mod.rs @@ -0,0 +1 @@ +pub mod indexes; diff --git a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/bills.rs b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/bills.rs index a3a6911b98..93d0ffe61a 100644 --- a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/bills.rs +++ b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/bills.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "bills")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users.rs b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users.rs index 50765913e2..36dca0cf41 100644 --- a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users.rs +++ b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_saved_bills.rs b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_saved_bills.rs index 4ef0281cd2..3bf1025e10 100644 --- a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_saved_bills.rs +++ b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_saved_bills.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users_saved_bills")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_votes.rs b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_votes.rs index a04ad7fa73..169b2cfaa6 100644 --- a/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_votes.rs +++ b/sea-orm-codegen/src/tests_cfg/duplicated_many_to_many_paths/users_votes.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users_votes")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/sea-orm-codegen/src/tests_cfg/many_to_many/bills.rs b/sea-orm-codegen/src/tests_cfg/many_to_many/bills.rs index 5a3acef9cb..82ca862ccd 100644 --- a/sea-orm-codegen/src/tests_cfg/many_to_many/bills.rs +++ b/sea-orm-codegen/src/tests_cfg/many_to_many/bills.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "bills")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/many_to_many/users.rs b/sea-orm-codegen/src/tests_cfg/many_to_many/users.rs index 3afd1cd517..35bc9e8073 100644 --- a/sea-orm-codegen/src/tests_cfg/many_to_many/users.rs +++ b/sea-orm-codegen/src/tests_cfg/many_to_many/users.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/many_to_many/users_votes.rs b/sea-orm-codegen/src/tests_cfg/many_to_many/users_votes.rs index a04ad7fa73..169b2cfaa6 100644 --- a/sea-orm-codegen/src/tests_cfg/many_to_many/users_votes.rs +++ b/sea-orm-codegen/src/tests_cfg/many_to_many/users_votes.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users_votes")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/bills.rs b/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/bills.rs index 5094e5885f..a16d6b5908 100644 --- a/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/bills.rs +++ b/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/bills.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "bills")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users.rs b/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users.rs index 99189cea48..9c27292e30 100644 --- a/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users.rs +++ b/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users_votes.rs b/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users_votes.rs index 87187f4c0b..dcf08feb66 100644 --- a/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users_votes.rs +++ b/sea-orm-codegen/src/tests_cfg/many_to_many_multiple/users_votes.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users_votes")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/sea-orm-codegen/src/tests_cfg/mod.rs b/sea-orm-codegen/src/tests_cfg/mod.rs index 8032c76015..154a90e5a1 100644 --- a/sea-orm-codegen/src/tests_cfg/mod.rs +++ b/sea-orm-codegen/src/tests_cfg/mod.rs @@ -1,3 +1,7 @@ +#![allow(unused_imports, dead_code)] + +pub mod compact; +pub mod dense; pub mod duplicated_many_to_many_paths; pub mod many_to_many; pub mod many_to_many_multiple; diff --git a/sea-orm-codegen/src/tests_cfg/self_referencing/bills.rs b/sea-orm-codegen/src/tests_cfg/self_referencing/bills.rs index 583655f673..26527c09a7 100644 --- a/sea-orm-codegen/src/tests_cfg/self_referencing/bills.rs +++ b/sea-orm-codegen/src/tests_cfg/self_referencing/bills.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "bills")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/tests_cfg/self_referencing/users.rs b/sea-orm-codegen/src/tests_cfg/self_referencing/users.rs index b54f3e5e2d..212819b21f 100644 --- a/sea-orm-codegen/src/tests_cfg/self_referencing/users.rs +++ b/sea-orm-codegen/src/tests_cfg/self_referencing/users.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/src/util.rs b/sea-orm-codegen/src/util.rs index 7005b19d02..46e7b9853f 100644 --- a/sea-orm-codegen/src/util.rs +++ b/sea-orm-codegen/src/util.rs @@ -1,5 +1,3 @@ -use sea_query::TableRef; - pub(crate) fn escape_rust_keyword(string: T) -> String where T: ToString, @@ -23,17 +21,3 @@ pub(crate) const RUST_KEYWORDS: [&str; 49] = [ ]; pub(crate) const RUST_SPECIAL_KEYWORDS: [&str; 3] = ["crate", "Self", "self"]; - -pub(crate) fn unpack_table_ref(table_ref: &TableRef) -> String { - match table_ref { - TableRef::Table(tbl) - | TableRef::SchemaTable(_, tbl) - | TableRef::DatabaseSchemaTable(_, _, tbl) - | TableRef::TableAlias(tbl, _) - | TableRef::SchemaTableAlias(_, tbl, _) - | TableRef::DatabaseSchemaTableAlias(_, _, tbl, _) - | TableRef::SubQuery(_, tbl) - | TableRef::ValuesList(_, tbl) - | TableRef::FunctionCall(_, tbl) => tbl.to_string(), - } -} diff --git a/sea-orm-codegen/tests/compact/cake.rs b/sea-orm-codegen/tests/compact/cake.rs index 7451140d09..9db4343ced 100644 --- a/sea-orm-codegen/tests/compact/cake.rs +++ b/sea-orm-codegen/tests/compact/cake.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact/cake_filling.rs b/sea-orm-codegen/tests/compact/cake_filling.rs index de27153dd8..710d03ac1b 100644 --- a/sea-orm-codegen/tests/compact/cake_filling.rs +++ b/sea-orm-codegen/tests/compact/cake_filling.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "_cake_filling_")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/sea-orm-codegen/tests/compact/cake_filling_price.rs b/sea-orm-codegen/tests/compact/cake_filling_price.rs new file mode 100644 index 0000000000..5e32e7d77c --- /dev/null +++ b/sea-orm-codegen/tests/compact/cake_filling_price.rs @@ -0,0 +1,31 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_filling_price")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub filling_id: i32, + pub price: Decimal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::cake_filling::Entity", + from = "(Column::CakeId, Column::FillingId)", + to = "(super::cake_filling::Column::CakeId, super::cake_filling::Column::FillingId)", + )] + CakeFilling, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/sea-orm-codegen/tests/compact/child.rs b/sea-orm-codegen/tests/compact/child.rs new file mode 100644 index 0000000000..81c696d14c --- /dev/null +++ b/sea-orm-codegen/tests/compact/child.rs @@ -0,0 +1,30 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "child")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub parent_id1: i32, + pub parent_id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::parent::Entity", + from = "(Column::ParentId1, Column::ParentId2)", + to = "(super::parent::Column::Id1, super::parent::Column::Id2)", + )] + Parent, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Parent.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/compact/collection.rs b/sea-orm-codegen/tests/compact/collection.rs index e272ac319a..ce2a8b217a 100644 --- a/sea-orm-codegen/tests/compact/collection.rs +++ b/sea-orm-codegen/tests/compact/collection.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "collection")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact/filling.rs b/sea-orm-codegen/tests/compact/filling.rs index 58f5f05cd1..2da4221f81 100644 --- a/sea-orm-codegen/tests/compact/filling.rs +++ b/sea-orm-codegen/tests/compact/filling.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "filling")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact/fruit.rs b/sea-orm-codegen/tests/compact/fruit.rs index 6ead03d1aa..4898a0f891 100644 --- a/sea-orm-codegen/tests/compact/fruit.rs +++ b/sea-orm-codegen/tests/compact/fruit.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "fruit")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact/parent.rs b/sea-orm-codegen/tests/compact/parent.rs new file mode 100644 index 0000000000..68e0665e24 --- /dev/null +++ b/sea-orm-codegen/tests/compact/parent.rs @@ -0,0 +1,26 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "parent")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id1: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::child::Entity")] + Child, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Child.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/compact/rust_keyword.rs b/sea-orm-codegen/tests/compact/rust_keyword.rs index 1daeba8ebb..3c1be92a2f 100644 --- a/sea-orm-codegen/tests/compact/rust_keyword.rs +++ b/sea-orm-codegen/tests/compact/rust_keyword.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "rust_keyword")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact/vendor.rs b/sea-orm-codegen/tests/compact/vendor.rs index f14c280842..7d686cdd82 100644 --- a/sea-orm-codegen/tests/compact/vendor.rs +++ b/sea-orm-codegen/tests/compact/vendor.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "vendor")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_attributes/cake_multiple.rs b/sea-orm-codegen/tests/compact_with_attributes/cake_multiple.rs index 795a3b9b20..a2b12f91d0 100644 --- a/sea-orm-codegen/tests/compact_with_attributes/cake_multiple.rs +++ b/sea-orm-codegen/tests/compact_with_attributes/cake_multiple.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] #[serde(rename_all = "camelCase")] #[ts(export)] diff --git a/sea-orm-codegen/tests/compact_with_attributes/cake_none.rs b/sea-orm-codegen/tests/compact_with_attributes/cake_none.rs index d72ea6b214..413e9bd409 100644 --- a/sea-orm-codegen/tests/compact_with_attributes/cake_none.rs +++ b/sea-orm-codegen/tests/compact_with_attributes/cake_none.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_attributes/cake_one.rs b/sea-orm-codegen/tests/compact_with_attributes/cake_one.rs index 11162ca1e9..0cf7c15d69 100644 --- a/sea-orm-codegen/tests/compact_with_attributes/cake_one.rs +++ b/sea-orm-codegen/tests/compact_with_attributes/cake_one.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] #[serde(rename_all = "camelCase")] pub struct Model { diff --git a/sea-orm-codegen/tests/compact_with_derives/cake_multiple.rs b/sea-orm-codegen/tests/compact_with_derives/cake_multiple.rs index dd69de77c6..531a78573d 100644 --- a/sea-orm-codegen/tests/compact_with_derives/cake_multiple.rs +++ b/sea-orm-codegen/tests/compact_with_derives/cake_multiple.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, ts_rs::TS, utoipa::ToSchema)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, ts_rs::TS, utoipa::ToSchema)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_derives/cake_none.rs b/sea-orm-codegen/tests/compact_with_derives/cake_none.rs index d72ea6b214..413e9bd409 100644 --- a/sea-orm-codegen/tests/compact_with_derives/cake_none.rs +++ b/sea-orm-codegen/tests/compact_with_derives/cake_none.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_derives/cake_one.rs b/sea-orm-codegen/tests/compact_with_derives/cake_one.rs index 9a4aeaba08..34dba4daf4 100644 --- a/sea-orm-codegen/tests/compact_with_derives/cake_one.rs +++ b/sea-orm-codegen/tests/compact_with_derives/cake_one.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, ts_rs::TS)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, ts_rs::TS)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/cake.rs b/sea-orm-codegen/tests/compact_with_schema_name/cake.rs index b8418d646c..236251251f 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/cake.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/cake.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/cake_filling.rs b/sea-orm-codegen/tests/compact_with_schema_name/cake_filling.rs index 7ca8eb91fa..02170070cf 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/cake_filling.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/cake_filling.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "_cake_filling_")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/cake_filling_price.rs b/sea-orm-codegen/tests/compact_with_schema_name/cake_filling_price.rs new file mode 100644 index 0000000000..8808936804 --- /dev/null +++ b/sea-orm-codegen/tests/compact_with_schema_name/cake_filling_price.rs @@ -0,0 +1,31 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(schema_name = "schema_name", table_name = "cake_filling_price")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub filling_id: i32, + pub price: Decimal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::cake_filling::Entity", + from = "(Column::CakeId, Column::FillingId)", + to = "(super::cake_filling::Column::CakeId, super::cake_filling::Column::FillingId)", + )] + CakeFilling, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/sea-orm-codegen/tests/compact_with_schema_name/child.rs b/sea-orm-codegen/tests/compact_with_schema_name/child.rs new file mode 100644 index 0000000000..b9b319b797 --- /dev/null +++ b/sea-orm-codegen/tests/compact_with_schema_name/child.rs @@ -0,0 +1,30 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(schema_name = "schema_name", table_name = "child")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub parent_id1: i32, + pub parent_id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::parent::Entity", + from = "(Column::ParentId1, Column::ParentId2)", + to = "(super::parent::Column::Id1, super::parent::Column::Id2)", + )] + Parent, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Parent.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/compact_with_schema_name/collection.rs b/sea-orm-codegen/tests/compact_with_schema_name/collection.rs index 5fe8d73c34..0732f6baf7 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/collection.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/collection.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "collection")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/filling.rs b/sea-orm-codegen/tests/compact_with_schema_name/filling.rs index f68ca06c96..01d12926a1 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/filling.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/filling.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "filling")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/fruit.rs b/sea-orm-codegen/tests/compact_with_schema_name/fruit.rs index dc446b1d11..2f68c9d917 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/fruit.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/fruit.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "fruit")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/parent.rs b/sea-orm-codegen/tests/compact_with_schema_name/parent.rs new file mode 100644 index 0000000000..3eb41c7739 --- /dev/null +++ b/sea-orm-codegen/tests/compact_with_schema_name/parent.rs @@ -0,0 +1,26 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(schema_name = "schema_name", table_name = "parent")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id1: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::child::Entity")] + Child, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Child.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/compact_with_schema_name/rust_keyword.rs b/sea-orm-codegen/tests/compact_with_schema_name/rust_keyword.rs index 014836eacb..b2b112f210 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/rust_keyword.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/rust_keyword.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "rust_keyword")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_schema_name/vendor.rs b/sea-orm-codegen/tests/compact_with_schema_name/vendor.rs index c3909880ca..40b269ec41 100644 --- a/sea-orm-codegen/tests/compact_with_schema_name/vendor.rs +++ b/sea-orm-codegen/tests/compact_with_schema_name/vendor.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(schema_name = "schema_name", table_name = "vendor")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_serde/cake_both.rs b/sea-orm-codegen/tests/compact_with_serde/cake_both.rs index fa893a5c29..bdeeeb036e 100644 --- a/sea-orm-codegen/tests/compact_with_serde/cake_both.rs +++ b/sea-orm-codegen/tests/compact_with_serde/cake_both.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude:: * ; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_serde/cake_deserialize.rs b/sea-orm-codegen/tests/compact_with_serde/cake_deserialize.rs index 620bdf67a5..f98e7f6959 100644 --- a/sea-orm-codegen/tests/compact_with_serde/cake_deserialize.rs +++ b/sea-orm-codegen/tests/compact_with_serde/cake_deserialize.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude:: * ; use serde::Deserialize; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_serde/cake_none.rs b/sea-orm-codegen/tests/compact_with_serde/cake_none.rs index d72ea6b214..413e9bd409 100644 --- a/sea-orm-codegen/tests/compact_with_serde/cake_none.rs +++ b/sea-orm-codegen/tests/compact_with_serde/cake_none.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_serde/cake_serialize.rs b/sea-orm-codegen/tests/compact_with_serde/cake_serialize.rs index 77af4c5ac3..1b7bdb7fe9 100644 --- a/sea-orm-codegen/tests/compact_with_serde/cake_serialize.rs +++ b/sea-orm-codegen/tests/compact_with_serde/cake_serialize.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude:: * ; use serde::Serialize; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/compact_with_serde/cake_serialize_with_hidden_column.rs b/sea-orm-codegen/tests/compact_with_serde/cake_serialize_with_hidden_column.rs index 50aa9dcbba..9fe6a4ef05 100644 --- a/sea-orm-codegen/tests/compact_with_serde/cake_serialize_with_hidden_column.rs +++ b/sea-orm-codegen/tests/compact_with_serde/cake_serialize_with_hidden_column.rs @@ -3,7 +3,7 @@ use sea_orm::entity::prelude:: * ; use serde::Serialize; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/dense/cake.rs b/sea-orm-codegen/tests/dense/cake.rs new file mode 100644 index 0000000000..cf38c6dd5b --- /dev/null +++ b/sea-orm-codegen/tests/dense/cake.rs @@ -0,0 +1,19 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", nullable)] + pub name: Option , + #[sea_orm(has_many)] + pub fruits: HasMany , + #[sea_orm(has_many, via = "cake_filling")] + pub fillings: HasMany , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/cake_filling.rs b/sea-orm-codegen/tests/dense/cake_filling.rs new file mode 100644 index 0000000000..035f681637 --- /dev/null +++ b/sea-orm-codegen/tests/dense/cake_filling.rs @@ -0,0 +1,19 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "_cake_filling_")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub filling_id: i32, + #[sea_orm(belongs_to, from = "cake_id", to = "id", on_update = "Cascade", on_delete = "Cascade")] + pub cake: HasOne , + #[sea_orm(belongs_to, from = "filling_id", to = "id", on_update = "Cascade", on_delete = "Cascade")] + pub filling: HasOne , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/cake_filling_price.rs b/sea-orm-codegen/tests/dense/cake_filling_price.rs new file mode 100644 index 0000000000..3d0f6c0807 --- /dev/null +++ b/sea-orm-codegen/tests/dense/cake_filling_price.rs @@ -0,0 +1,22 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_filling_price")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub cake_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub filling_id: i32, + pub price: Decimal, + #[sea_orm( + belongs_to, + from = "(cake_id, filling_id)", + to = "(cake_id, filling_id)" + )] + pub cake_filling: HasOne , +} + +impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/sea-orm-codegen/tests/dense/cake_with_double.rs b/sea-orm-codegen/tests/dense/cake_with_double.rs new file mode 100644 index 0000000000..2f792dbd8b --- /dev/null +++ b/sea-orm-codegen/tests/dense/cake_with_double.rs @@ -0,0 +1,21 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_with_double")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", nullable)] + pub name: Option , + #[sea_orm(column_type = "Double", nullable)] + pub price: Option , + #[sea_orm(has_many)] + pub fruits: HasMany , + #[sea_orm(has_many, via = "cake_filling")] + pub fillings: HasMany , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/cake_with_float.rs b/sea-orm-codegen/tests/dense/cake_with_float.rs new file mode 100644 index 0000000000..626339bf5d --- /dev/null +++ b/sea-orm-codegen/tests/dense/cake_with_float.rs @@ -0,0 +1,21 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "cake_with_float")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", nullable)] + pub name: Option , + #[sea_orm(column_type = "Float", nullable)] + pub price: Option , + #[sea_orm(has_many)] + pub fruits: HasMany , + #[sea_orm(has_many, via = "cake_filling")] + pub fillings: HasMany , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/child.rs b/sea-orm-codegen/tests/dense/child.rs new file mode 100644 index 0000000000..55d1779cf4 --- /dev/null +++ b/sea-orm-codegen/tests/dense/child.rs @@ -0,0 +1,17 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "child")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub parent_id1: i32, + pub parent_id2: i32, + #[sea_orm(belongs_to, from = "(parent_id1, parent_id2)", to = "(id1, id2)")] + pub parent: HasOne , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/collection.rs b/sea-orm-codegen/tests/dense/collection.rs new file mode 100644 index 0000000000..3e174b0ea4 --- /dev/null +++ b/sea-orm-codegen/tests/dense/collection.rs @@ -0,0 +1,15 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "collection")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub integers: Vec , + pub integers_opt: Option > , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/collection_float.rs b/sea-orm-codegen/tests/dense/collection_float.rs new file mode 100644 index 0000000000..db89a48460 --- /dev/null +++ b/sea-orm-codegen/tests/dense/collection_float.rs @@ -0,0 +1,15 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "collection_float")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub floats: Vec , + pub doubles: Vec , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/filling.rs b/sea-orm-codegen/tests/dense/filling.rs new file mode 100644 index 0000000000..63f426d418 --- /dev/null +++ b/sea-orm-codegen/tests/dense/filling.rs @@ -0,0 +1,16 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "filling")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(has_many, via = "cake_filling")] + pub cakes: HasMany , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/fruit.rs b/sea-orm-codegen/tests/dense/fruit.rs new file mode 100644 index 0000000000..697fe8316b --- /dev/null +++ b/sea-orm-codegen/tests/dense/fruit.rs @@ -0,0 +1,19 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "fruit")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub cake_id: Option , + #[sea_orm(belongs_to, from = "cake_id", to = "id")] + pub cake: HasOne , + #[sea_orm(has_many)] + pub vendors: HasMany , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/mod.rs b/sea-orm-codegen/tests/dense/mod.rs new file mode 100644 index 0000000000..5a8c6c21c1 --- /dev/null +++ b/sea-orm-codegen/tests/dense/mod.rs @@ -0,0 +1,9 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub mod prelude; + +pub mod cake; +pub mod cake_filling; +pub mod filling; +pub mod fruit; +pub mod vendor; diff --git a/sea-orm-codegen/tests/dense/parent.rs b/sea-orm-codegen/tests/dense/parent.rs new file mode 100644 index 0000000000..6c9e454ce0 --- /dev/null +++ b/sea-orm-codegen/tests/dense/parent.rs @@ -0,0 +1,17 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "parent")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id1: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub id2: i32, + #[sea_orm(has_many)] + pub children: HasMany , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/basic/src/entities.rs b/sea-orm-codegen/tests/dense/prelude.rs similarity index 64% rename from examples/basic/src/entities.rs rename to sea-orm-codegen/tests/dense/prelude.rs index 40a4bd9a38..b4e85c7831 100644 --- a/examples/basic/src/entities.rs +++ b/sea-orm-codegen/tests/dense/prelude.rs @@ -1,4 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + pub use super::cake::Entity as Cake; pub use super::cake_filling::Entity as CakeFilling; pub use super::filling::Entity as Filling; pub use super::fruit::Entity as Fruit; +pub use super::vendor::Entity as Vendor; diff --git a/sea-orm-codegen/tests/dense/rust_keyword.rs b/sea-orm-codegen/tests/dense/rust_keyword.rs new file mode 100644 index 0000000000..d62cf5ade4 --- /dev/null +++ b/sea-orm-codegen/tests/dense/rust_keyword.rs @@ -0,0 +1,35 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "rust_keyword")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub testing: i8, + pub rust: u8, + pub keywords: i16, + pub r#type: u16, + pub r#typeof: i32, + pub crate_: u32, + pub self_: i64, + pub self_id1: u64, + pub self_id2: i32, + pub fruit_id1: i32, + pub fruit_id2: i32, + pub cake_id: i32, + #[sea_orm(self_ref, relation_enum = "SelfRef1", from = "self_id1", to = "id")] + pub rust_keyword_1: HasOne , + #[sea_orm(self_ref, relation_enum = "SelfRef2", from = "self_id2", to = "id")] + pub rust_keyword_2: HasOne , + #[sea_orm(belongs_to, relation_enum = "Fruit1", from = "fruit_id1", to = "id")] + pub fruit_1: HasOne , + #[sea_orm(belongs_to, relation_enum = "Fruit2", from = "fruit_id2", to = "id")] + pub fruit_2: HasOne , + #[sea_orm(belongs_to, from = "cake_id", to = "id")] + pub cake: HasOne , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/dense/vendor.rs b/sea-orm-codegen/tests/dense/vendor.rs new file mode 100644 index 0000000000..0c48ad0f93 --- /dev/null +++ b/sea-orm-codegen/tests/dense/vendor.rs @@ -0,0 +1,19 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "vendor")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_name = "_name_")] + pub name: String, + #[sea_orm(column_name = "fruitId")] + pub fruit_id: Option , + #[sea_orm(belongs_to, from = "fruit_id", to = "id")] + pub fruit: HasOne , +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded/cake.rs b/sea-orm-codegen/tests/expanded/cake.rs index 961b19192d..3bc39f8377 100644 --- a/sea-orm-codegen/tests/expanded/cake.rs +++ b/sea-orm-codegen/tests/expanded/cake.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded/cake_filling.rs b/sea-orm-codegen/tests/expanded/cake_filling.rs index 92e157f287..0e8218c937 100644 --- a/sea-orm-codegen/tests/expanded/cake_filling.rs +++ b/sea-orm-codegen/tests/expanded/cake_filling.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "_cake_filling_" } } diff --git a/examples/basic/src/example_cake_filling.rs b/sea-orm-codegen/tests/expanded/cake_filling_price.rs similarity index 55% rename from examples/basic/src/example_cake_filling.rs rename to sea-orm-codegen/tests/expanded/cake_filling_price.rs index 741ec120b6..66f9918bb5 100644 --- a/examples/basic/src/example_cake_filling.rs +++ b/sea-orm-codegen/tests/expanded/cake_filling_price.rs @@ -1,41 +1,33 @@ -//! The `cake_filling` entity. +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 use sea_orm::entity::prelude::*; -/// CakeFilling entity #[derive(Copy, Clone, Default, Debug, DeriveEntity)] pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { - "cake_filling" + fn table_name(&self) -> & 'static str { + "cake_filling_price" } } -/// CakeFilling model -#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)] +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] pub struct Model { - /// cake_id field pub cake_id: i32, - /// filling_id field pub filling_id: i32, + pub price: Decimal, } -/// CakeFilling column #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] pub enum Column { - /// CakeId column CakeId, - /// FillingId column FillingId, + Price, } -/// CakeFilling primary key #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] pub enum PrimaryKey { - /// CakeId primary key CakeId, - /// FillingId primary key FillingId, } @@ -47,13 +39,9 @@ impl PrimaryKeyTrait for PrimaryKey { } } -/// CakeFilling relation #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { - /// Cake relation - Cake, - /// Filling relation - Filling, + CakeFilling, } impl ColumnTrait for Column { @@ -63,6 +51,7 @@ impl ColumnTrait for Column { match self { Self::CakeId => ColumnType::Integer.def(), Self::FillingId => ColumnType::Integer.def(), + Self::Price => ColumnType::Decimal(None).def(), } } } @@ -70,16 +59,21 @@ impl ColumnTrait for Column { impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Cake => Entity::belongs_to(super::cake::Entity) - .from(Column::CakeId) - .to(super::cake::Column::Id) - .into(), - Self::Filling => Entity::belongs_to(super::filling::Entity) - .from(Column::FillingId) - .to(super::filling::Column::Id) + Self::CakeFilling => Entity::belongs_to(super::cake_filling::Entity) + .from((Column::CakeId, Column::FillingId)) + .to(( + super::cake_filling::Column::CakeId, + super::cake_filling::Column::FillingId + )) .into(), } } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded/cake_with_double.rs b/sea-orm-codegen/tests/expanded/cake_with_double.rs index d71a9fbd3e..ff4b0c4272 100644 --- a/sea-orm-codegen/tests/expanded/cake_with_double.rs +++ b/sea-orm-codegen/tests/expanded/cake_with_double.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake_with_double" } } diff --git a/sea-orm-codegen/tests/expanded/cake_with_float.rs b/sea-orm-codegen/tests/expanded/cake_with_float.rs index d5059be167..6e8a34abee 100644 --- a/sea-orm-codegen/tests/expanded/cake_with_float.rs +++ b/sea-orm-codegen/tests/expanded/cake_with_float.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake_with_float" } } diff --git a/sea-orm-codegen/tests/expanded/child.rs b/sea-orm-codegen/tests/expanded/child.rs new file mode 100644 index 0000000000..31f9b5436d --- /dev/null +++ b/sea-orm-codegen/tests/expanded/child.rs @@ -0,0 +1,73 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> & 'static str { + "child" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id: i32, + pub parent_id1: i32, + pub parent_id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + ParentId1, + ParentId2, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Parent, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::ParentId1 => ColumnType::Integer.def(), + Self::ParentId2 => ColumnType::Integer.def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Parent => Entity::belongs_to(super::parent::Entity) + .from((Column::ParentId1, Column::ParentId2)) + .to((super::parent::Column::Id1, super::parent::Column::Id2)) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Parent.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded/collection.rs b/sea-orm-codegen/tests/expanded/collection.rs index fce6642ef6..fa22aae532 100644 --- a/sea-orm-codegen/tests/expanded/collection.rs +++ b/sea-orm-codegen/tests/expanded/collection.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "collection" } } diff --git a/sea-orm-codegen/tests/expanded/collection_float.rs b/sea-orm-codegen/tests/expanded/collection_float.rs index 6d6dff7934..5981177326 100644 --- a/sea-orm-codegen/tests/expanded/collection_float.rs +++ b/sea-orm-codegen/tests/expanded/collection_float.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "collection_float" } } diff --git a/sea-orm-codegen/tests/expanded/filling.rs b/sea-orm-codegen/tests/expanded/filling.rs index 22035244f4..86be2bbe2f 100644 --- a/sea-orm-codegen/tests/expanded/filling.rs +++ b/sea-orm-codegen/tests/expanded/filling.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "filling" } } @@ -44,7 +44,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(Some(255u32)).def(), + Self::Name => ColumnType::String(StringLen::N(255u32)).def(), } } } diff --git a/sea-orm-codegen/tests/expanded/fruit.rs b/sea-orm-codegen/tests/expanded/fruit.rs index df1618cad7..3ac33d3e21 100644 --- a/sea-orm-codegen/tests/expanded/fruit.rs +++ b/sea-orm-codegen/tests/expanded/fruit.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "fruit" } } @@ -49,7 +49,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(Some(255u32)).def(), + Self::Name => ColumnType::String(StringLen::N(255u32)).def(), Self::CakeId => ColumnType::Integer.def().null(), } } diff --git a/sea-orm-codegen/tests/expanded/parent.rs b/sea-orm-codegen/tests/expanded/parent.rs new file mode 100644 index 0000000000..8a6e939880 --- /dev/null +++ b/sea-orm-codegen/tests/expanded/parent.rs @@ -0,0 +1,68 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> & 'static str { + "parent" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id1: i32, + pub id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id1, + Id2, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id1, + Id2, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Child, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id1 => ColumnType::Integer.def(), + Self::Id2 => ColumnType::Integer.def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Child => Entity::has_many(super::child::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Child.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded/rust_keyword.rs b/sea-orm-codegen/tests/expanded/rust_keyword.rs index 8cdec4d83e..019c7167af 100644 --- a/sea-orm-codegen/tests/expanded/rust_keyword.rs +++ b/sea-orm-codegen/tests/expanded/rust_keyword.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "rust_keyword" } } diff --git a/sea-orm-codegen/tests/expanded/vendor.rs b/sea-orm-codegen/tests/expanded/vendor.rs index 91cbbd47a8..28f1d14566 100644 --- a/sea-orm-codegen/tests/expanded/vendor.rs +++ b/sea-orm-codegen/tests/expanded/vendor.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "vendor" } } @@ -50,7 +50,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(Some(255u32)).def(), + Self::Name => ColumnType::String(StringLen::N(255u32)).def(), Self::FruitId => ColumnType::Integer.def().null(), } } diff --git a/sea-orm-codegen/tests/expanded_with_attributes/cake_multiple.rs b/sea-orm-codegen/tests/expanded_with_attributes/cake_multiple.rs index 5c498d83c5..fb47895f37 100644 --- a/sea-orm-codegen/tests/expanded_with_attributes/cake_multiple.rs +++ b/sea-orm-codegen/tests/expanded_with_attributes/cake_multiple.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_attributes/cake_none.rs b/sea-orm-codegen/tests/expanded_with_attributes/cake_none.rs index a540fad138..c38c27deca 100644 --- a/sea-orm-codegen/tests/expanded_with_attributes/cake_none.rs +++ b/sea-orm-codegen/tests/expanded_with_attributes/cake_none.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_attributes/cake_one.rs b/sea-orm-codegen/tests/expanded_with_attributes/cake_one.rs index 77da3181a4..6a8c252a52 100644 --- a/sea-orm-codegen/tests/expanded_with_attributes/cake_one.rs +++ b/sea-orm-codegen/tests/expanded_with_attributes/cake_one.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_column_derives/cake_multiple.rs b/sea-orm-codegen/tests/expanded_with_column_derives/cake_multiple.rs new file mode 100644 index 0000000000..8d275e606f --- /dev/null +++ b/sea-orm-codegen/tests/expanded_with_column_derives/cake_multiple.rs @@ -0,0 +1,77 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude:: * ; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> & 'static str { + "cake" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn, async_graphql::Enum, Eq, PartialEq)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Fruit, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::Text.def().null(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Fruit => Entity::has_many(super::fruit::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::cake_filling::Relation::Filling.def() + } + fn via() -> Option { + Some(super::cake_filling::Relation::Cake.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded_with_column_derives/cake_none.rs b/sea-orm-codegen/tests/expanded_with_column_derives/cake_none.rs new file mode 100644 index 0000000000..c38c27deca --- /dev/null +++ b/sea-orm-codegen/tests/expanded_with_column_derives/cake_none.rs @@ -0,0 +1,77 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude:: * ; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> & 'static str { + "cake" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Fruit, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::Text.def().null(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Fruit => Entity::has_many(super::fruit::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::cake_filling::Relation::Filling.def() + } + fn via() -> Option { + Some(super::cake_filling::Relation::Cake.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded_with_column_derives/cake_one.rs b/sea-orm-codegen/tests/expanded_with_column_derives/cake_one.rs new file mode 100644 index 0000000000..e8dcef8d7c --- /dev/null +++ b/sea-orm-codegen/tests/expanded_with_column_derives/cake_one.rs @@ -0,0 +1,77 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude:: * ; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> & 'static str { + "cake" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn, async_graphql::Enum)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Fruit, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::Text.def().null(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Fruit => Entity::has_many(super::fruit::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::cake_filling::Relation::Filling.def() + } + fn via() -> Option { + Some(super::cake_filling::Relation::Cake.def().rev()) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded_with_derives/cake_multiple.rs b/sea-orm-codegen/tests/expanded_with_derives/cake_multiple.rs index c04dfdd4d6..60f2920a42 100644 --- a/sea-orm-codegen/tests/expanded_with_derives/cake_multiple.rs +++ b/sea-orm-codegen/tests/expanded_with_derives/cake_multiple.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_derives/cake_none.rs b/sea-orm-codegen/tests/expanded_with_derives/cake_none.rs index a540fad138..c38c27deca 100644 --- a/sea-orm-codegen/tests/expanded_with_derives/cake_none.rs +++ b/sea-orm-codegen/tests/expanded_with_derives/cake_none.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_derives/cake_one.rs b/sea-orm-codegen/tests/expanded_with_derives/cake_one.rs index a4ebdf829f..9b57a1d172 100644 --- a/sea-orm-codegen/tests/expanded_with_derives/cake_one.rs +++ b/sea-orm-codegen/tests/expanded_with_derives/cake_one.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/cake.rs b/sea-orm-codegen/tests/expanded_with_schema_name/cake.rs index 72bdb0b171..148bff4472 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/cake.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/cake.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling.rs b/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling.rs index 0113751f0f..7aff5f0690 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "_cake_filling_" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling_price.rs b/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling_price.rs new file mode 100644 index 0000000000..c935ab5fbf --- /dev/null +++ b/sea-orm-codegen/tests/expanded_with_schema_name/cake_filling_price.rs @@ -0,0 +1,83 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn schema_name(&self) -> Option< &str > { + Some("schema_name") + } + + fn table_name(&self) -> & 'static str { + "cake_filling_price" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, + pub price: Decimal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + CakeId, + FillingId, + Price, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + CakeId, + FillingId, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + CakeFilling, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Self::CakeId => ColumnType::Integer.def(), + Self::FillingId => ColumnType::Integer.def(), + Self::Price => ColumnType::Decimal(None).def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::CakeFilling => Entity::belongs_to(super::cake_filling::Entity) + .from((Column::CakeId, Column::FillingId)) + .to(( + super::cake_filling::Column::CakeId, + super::cake_filling::Column::FillingId + )) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_double.rs b/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_double.rs index 0956e3e507..5e6e8f6302 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_double.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_double.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake_with_double" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_float.rs b/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_float.rs index b3256ca2a5..0f054cc67d 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_float.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/cake_with_float.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake_with_float" } } diff --git a/examples/basic/src/example_fruit.rs b/sea-orm-codegen/tests/expanded_with_schema_name/child.rs similarity index 53% rename from examples/basic/src/example_fruit.rs rename to sea-orm-codegen/tests/expanded_with_schema_name/child.rs index 59a072e9c3..39fb3a3f2c 100644 --- a/examples/basic/src/example_fruit.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/child.rs @@ -1,69 +1,58 @@ -//! The `fruit` entity. +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 use sea_orm::entity::prelude::*; -/// Fruit entity #[derive(Copy, Clone, Default, Debug, DeriveEntity)] pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { - "fruit" + fn schema_name(&self) -> Option< &str > { + Some("schema_name") + } + + fn table_name(&self) -> & 'static str { + "child" } } -/// Fruit model -#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)] +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] pub struct Model { - /// id field pub id: i32, - /// name field - pub name: String, - /// cake_id field - pub cake_id: Option, + pub parent_id1: i32, + pub parent_id2: i32, } -/// Fruit column #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] pub enum Column { - /// Id column Id, - /// Name column - Name, - /// CakeId column - CakeId, + ParentId1, + ParentId2, } -/// Fruit primary key #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] pub enum PrimaryKey { - /// Id primary key Id, } impl PrimaryKeyTrait for PrimaryKey { type ValueType = i32; - fn auto_increment() -> bool { true } } -/// Fruit relation #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { - /// Cake relation - Cake, + Parent, } impl ColumnTrait for Column { type EntityName = Entity; - fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(None).def(), - Self::CakeId => ColumnType::Integer.def(), + Self::ParentId1 => ColumnType::Integer.def(), + Self::ParentId2 => ColumnType::Integer.def(), } } } @@ -71,17 +60,17 @@ impl ColumnTrait for Column { impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Cake => Entity::belongs_to(super::cake::Entity) - .from(Column::CakeId) - .to(super::cake::Column::Id) + Self::Parent => Entity::belongs_to(super::parent::Entity) + .from((Column::ParentId1, Column::ParentId2)) + .to((super::parent::Column::Id1, super::parent::Column::Id2)) .into(), } } } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Cake.def() + Relation::Parent.def() } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/collection.rs b/sea-orm-codegen/tests/expanded_with_schema_name/collection.rs index 8fdd543831..8dea936dd3 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/collection.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/collection.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "collection" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/collection_float.rs b/sea-orm-codegen/tests/expanded_with_schema_name/collection_float.rs index 02a0bf6c67..6a38055ad3 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/collection_float.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/collection_float.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "collection_float" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/filling.rs b/sea-orm-codegen/tests/expanded_with_schema_name/filling.rs index 918fd1bfc3..c8cd9da4cc 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/filling.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/filling.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "filling" } } @@ -48,7 +48,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(Some(255u32)).def(), + Self::Name => ColumnType::String(StringLen::N(255u32)).def(), } } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/fruit.rs b/sea-orm-codegen/tests/expanded_with_schema_name/fruit.rs index e2268ba673..9430d6f837 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/fruit.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/fruit.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "fruit" } } @@ -53,7 +53,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(Some(255u32)).def(), + Self::Name => ColumnType::String(StringLen::N(255u32)).def(), Self::CakeId => ColumnType::Integer.def().null(), } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/parent.rs b/sea-orm-codegen/tests/expanded_with_schema_name/parent.rs new file mode 100644 index 0000000000..4fe3ef402b --- /dev/null +++ b/sea-orm-codegen/tests/expanded_with_schema_name/parent.rs @@ -0,0 +1,72 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn schema_name(&self) -> Option< &str > { + Some("schema_name") + } + + fn table_name(&self) -> & 'static str { + "parent" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id1: i32, + pub id2: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id1, + Id2, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id1, + Id2, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Child, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id1 => ColumnType::Integer.def(), + Self::Id2 => ColumnType::Integer.def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Child => Entity::has_many(super::child::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Child.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/rust_keyword.rs b/sea-orm-codegen/tests/expanded_with_schema_name/rust_keyword.rs index b402bdac6e..9657ad15c3 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/rust_keyword.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/rust_keyword.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "rust_keyword" } } diff --git a/sea-orm-codegen/tests/expanded_with_schema_name/vendor.rs b/sea-orm-codegen/tests/expanded_with_schema_name/vendor.rs index 1ad920abc6..6e5a109ed5 100644 --- a/sea-orm-codegen/tests/expanded_with_schema_name/vendor.rs +++ b/sea-orm-codegen/tests/expanded_with_schema_name/vendor.rs @@ -10,7 +10,7 @@ impl EntityName for Entity { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "vendor" } } @@ -54,7 +54,7 @@ impl ColumnTrait for Column { fn def(&self) -> ColumnDef { match self { Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(Some(255u32)).def(), + Self::Name => ColumnType::String(StringLen::N(255u32)).def(), Self::FruitId => ColumnType::Integer.def().null(), } } diff --git a/sea-orm-codegen/tests/expanded_with_serde/cake_both.rs b/sea-orm-codegen/tests/expanded_with_serde/cake_both.rs index 40c9e9987c..a0083dd794 100644 --- a/sea-orm-codegen/tests/expanded_with_serde/cake_both.rs +++ b/sea-orm-codegen/tests/expanded_with_serde/cake_both.rs @@ -7,7 +7,7 @@ use serde::{Deserialize,Serialize}; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_serde/cake_deserialize.rs b/sea-orm-codegen/tests/expanded_with_serde/cake_deserialize.rs index ec20107334..55e1405e3a 100644 --- a/sea-orm-codegen/tests/expanded_with_serde/cake_deserialize.rs +++ b/sea-orm-codegen/tests/expanded_with_serde/cake_deserialize.rs @@ -7,7 +7,7 @@ use serde::Deserialize; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_serde/cake_none.rs b/sea-orm-codegen/tests/expanded_with_serde/cake_none.rs index a540fad138..c38c27deca 100644 --- a/sea-orm-codegen/tests/expanded_with_serde/cake_none.rs +++ b/sea-orm-codegen/tests/expanded_with_serde/cake_none.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_serde/cake_serialize.rs b/sea-orm-codegen/tests/expanded_with_serde/cake_serialize.rs index 72a17d580f..d27eeebd38 100644 --- a/sea-orm-codegen/tests/expanded_with_serde/cake_serialize.rs +++ b/sea-orm-codegen/tests/expanded_with_serde/cake_serialize.rs @@ -7,7 +7,7 @@ use serde::Serialize; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/expanded_with_serde/cake_serialize_with_hidden_column.rs b/sea-orm-codegen/tests/expanded_with_serde/cake_serialize_with_hidden_column.rs index 74db30853a..172903cd4f 100644 --- a/sea-orm-codegen/tests/expanded_with_serde/cake_serialize_with_hidden_column.rs +++ b/sea-orm-codegen/tests/expanded_with_serde/cake_serialize_with_hidden_column.rs @@ -7,7 +7,7 @@ use serde::Serialize; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/frontend/cake.rs b/sea-orm-codegen/tests/frontend/cake.rs new file mode 100644 index 0000000000..00f20b80f1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/cake.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/cake_filling.rs b/sea-orm-codegen/tests/frontend/cake_filling.rs new file mode 100644 index 0000000000..0f6d6a5a92 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/cake_filling.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/cake_filling_price.rs b/sea-orm-codegen/tests/frontend/cake_filling_price.rs new file mode 100644 index 0000000000..361e5335de --- /dev/null +++ b/sea-orm-codegen/tests/frontend/cake_filling_price.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, + pub price: Decimal, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/cake_with_double.rs b/sea-orm-codegen/tests/frontend/cake_with_double.rs new file mode 100644 index 0000000000..c7a94cfd18 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/cake_with_double.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq)] +pub struct Model { + pub id: i32, + pub name: Option , + pub price: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/cake_with_float.rs b/sea-orm-codegen/tests/frontend/cake_with_float.rs new file mode 100644 index 0000000000..1b81519c14 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/cake_with_float.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq)] +pub struct Model { + pub id: i32, + pub name: Option , + pub price: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/child.rs b/sea-orm-codegen/tests/frontend/child.rs new file mode 100644 index 0000000000..d718351e76 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/child.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub parent_id1: i32, + pub parent_id2: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/collection.rs b/sea-orm-codegen/tests/frontend/collection.rs new file mode 100644 index 0000000000..7a69289eeb --- /dev/null +++ b/sea-orm-codegen/tests/frontend/collection.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub integers: Vec , + pub integers_opt: Option > , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/collection_float.rs b/sea-orm-codegen/tests/frontend/collection_float.rs new file mode 100644 index 0000000000..e06f620e5f --- /dev/null +++ b/sea-orm-codegen/tests/frontend/collection_float.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +#[derive(Clone, Debug, PartialEq)] +pub struct Model { + pub id: i32, + pub floats: Vec , + pub doubles: Vec , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/filling.rs b/sea-orm-codegen/tests/frontend/filling.rs new file mode 100644 index 0000000000..c8714eb399 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/filling.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: String, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/fruit.rs b/sea-orm-codegen/tests/frontend/fruit.rs new file mode 100644 index 0000000000..06e089a3fd --- /dev/null +++ b/sea-orm-codegen/tests/frontend/fruit.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: String, + pub cake_id: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/mod.rs b/sea-orm-codegen/tests/frontend/mod.rs new file mode 100644 index 0000000000..5a8c6c21c1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/mod.rs @@ -0,0 +1,9 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub mod prelude; + +pub mod cake; +pub mod cake_filling; +pub mod filling; +pub mod fruit; +pub mod vendor; diff --git a/sea-orm-codegen/tests/frontend/parent.rs b/sea-orm-codegen/tests/frontend/parent.rs new file mode 100644 index 0000000000..6b950aeec6 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/parent.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id1: i32, + pub id2: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/prelude.rs b/sea-orm-codegen/tests/frontend/prelude.rs new file mode 100644 index 0000000000..84dbb28ced --- /dev/null +++ b/sea-orm-codegen/tests/frontend/prelude.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub use super::cake::Model as Cake; +pub use super::cake_filling::Model as CakeFilling; +pub use super::filling::Model as Filling; +pub use super::fruit::Model as Fruit; +pub use super::vendor::Model as Vendor; diff --git a/sea-orm-codegen/tests/frontend/rust_keyword.rs b/sea-orm-codegen/tests/frontend/rust_keyword.rs new file mode 100644 index 0000000000..e085b33f17 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/rust_keyword.rs @@ -0,0 +1,18 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub testing: i8, + pub rust: u8, + pub keywords: i16, + pub r#type: u16, + pub r#typeof: i32, + pub crate_: u32, + pub self_: i64, + pub self_id1: u64, + pub self_id2: i32, + pub fruit_id1: i32, + pub fruit_id2: i32, + pub cake_id: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend/vendor.rs b/sea-orm-codegen/tests/frontend/vendor.rs new file mode 100644 index 0000000000..211c9ba1d1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend/vendor.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: String, + pub fruit_id: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_attributes/cake_multiple.rs b/sea-orm-codegen/tests/frontend_with_attributes/cake_multiple.rs new file mode 100644 index 0000000000..9f08e0437c --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_attributes/cake_multiple.rs @@ -0,0 +1,9 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_attributes/cake_none.rs b/sea-orm-codegen/tests/frontend_with_attributes/cake_none.rs new file mode 100644 index 0000000000..00f20b80f1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_attributes/cake_none.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_attributes/cake_one.rs b/sea-orm-codegen/tests/frontend_with_attributes/cake_one.rs new file mode 100644 index 0000000000..2e98067e1c --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_attributes/cake_one.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_derives/cake_multiple.rs b/sea-orm-codegen/tests/frontend_with_derives/cake_multiple.rs new file mode 100644 index 0000000000..699615cb4e --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_derives/cake_multiple.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq, ts_rs::TS, utoipa::ToSchema)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_derives/cake_none.rs b/sea-orm-codegen/tests/frontend_with_derives/cake_none.rs new file mode 100644 index 0000000000..00f20b80f1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_derives/cake_none.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_derives/cake_one.rs b/sea-orm-codegen/tests/frontend_with_derives/cake_one.rs new file mode 100644 index 0000000000..bbcc5c9d5c --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_derives/cake_one.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq, ts_rs::TS)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/cake.rs b/sea-orm-codegen/tests/frontend_with_schema_name/cake.rs new file mode 100644 index 0000000000..00f20b80f1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/cake.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/cake_filling.rs b/sea-orm-codegen/tests/frontend_with_schema_name/cake_filling.rs new file mode 100644 index 0000000000..0f6d6a5a92 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/cake_filling.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/cake_filling_price.rs b/sea-orm-codegen/tests/frontend_with_schema_name/cake_filling_price.rs new file mode 100644 index 0000000000..361e5335de --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/cake_filling_price.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, + pub price: Decimal, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/cake_with_double.rs b/sea-orm-codegen/tests/frontend_with_schema_name/cake_with_double.rs new file mode 100644 index 0000000000..c7a94cfd18 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/cake_with_double.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq)] +pub struct Model { + pub id: i32, + pub name: Option , + pub price: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/cake_with_float.rs b/sea-orm-codegen/tests/frontend_with_schema_name/cake_with_float.rs new file mode 100644 index 0000000000..1b81519c14 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/cake_with_float.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq)] +pub struct Model { + pub id: i32, + pub name: Option , + pub price: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/child.rs b/sea-orm-codegen/tests/frontend_with_schema_name/child.rs new file mode 100644 index 0000000000..d718351e76 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/child.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub parent_id1: i32, + pub parent_id2: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/collection.rs b/sea-orm-codegen/tests/frontend_with_schema_name/collection.rs new file mode 100644 index 0000000000..7a69289eeb --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/collection.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub integers: Vec , + pub integers_opt: Option > , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/collection_float.rs b/sea-orm-codegen/tests/frontend_with_schema_name/collection_float.rs new file mode 100644 index 0000000000..e06f620e5f --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/collection_float.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0 + +#[derive(Clone, Debug, PartialEq)] +pub struct Model { + pub id: i32, + pub floats: Vec , + pub doubles: Vec , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/filling.rs b/sea-orm-codegen/tests/frontend_with_schema_name/filling.rs new file mode 100644 index 0000000000..c8714eb399 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/filling.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: String, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/fruit.rs b/sea-orm-codegen/tests/frontend_with_schema_name/fruit.rs new file mode 100644 index 0000000000..06e089a3fd --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/fruit.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: String, + pub cake_id: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/mod.rs b/sea-orm-codegen/tests/frontend_with_schema_name/mod.rs new file mode 100644 index 0000000000..5a8c6c21c1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/mod.rs @@ -0,0 +1,9 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub mod prelude; + +pub mod cake; +pub mod cake_filling; +pub mod filling; +pub mod fruit; +pub mod vendor; diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/parent.rs b/sea-orm-codegen/tests/frontend_with_schema_name/parent.rs new file mode 100644 index 0000000000..6b950aeec6 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/parent.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id1: i32, + pub id2: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/prelude.rs b/sea-orm-codegen/tests/frontend_with_schema_name/prelude.rs new file mode 100644 index 0000000000..84dbb28ced --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/prelude.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub use super::cake::Model as Cake; +pub use super::cake_filling::Model as CakeFilling; +pub use super::filling::Model as Filling; +pub use super::fruit::Model as Fruit; +pub use super::vendor::Model as Vendor; diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/rust_keyword.rs b/sea-orm-codegen/tests/frontend_with_schema_name/rust_keyword.rs new file mode 100644 index 0000000000..e085b33f17 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/rust_keyword.rs @@ -0,0 +1,18 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub testing: i8, + pub rust: u8, + pub keywords: i16, + pub r#type: u16, + pub r#typeof: i32, + pub crate_: u32, + pub self_: i64, + pub self_id1: u64, + pub self_id2: i32, + pub fruit_id1: i32, + pub fruit_id2: i32, + pub cake_id: i32, +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_schema_name/vendor.rs b/sea-orm-codegen/tests/frontend_with_schema_name/vendor.rs new file mode 100644 index 0000000000..211c9ba1d1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_schema_name/vendor.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: String, + pub fruit_id: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_serde/cake_both.rs b/sea-orm-codegen/tests/frontend_with_serde/cake_both.rs new file mode 100644 index 0000000000..856353317c --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_serde/cake_both.rs @@ -0,0 +1,10 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Model { + #[serde(skip_deserializing)] + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_serde/cake_deserialize.rs b/sea-orm-codegen/tests/frontend_with_serde/cake_deserialize.rs new file mode 100644 index 0000000000..f1cff6ad90 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_serde/cake_deserialize.rs @@ -0,0 +1,10 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use serde::Deserialize; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct Model { + #[serde(skip_deserializing)] + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_serde/cake_none.rs b/sea-orm-codegen/tests/frontend_with_serde/cake_none.rs new file mode 100644 index 0000000000..00f20b80f1 --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_serde/cake_none.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_serde/cake_serialize.rs b/sea-orm-codegen/tests/frontend_with_serde/cake_serialize.rs new file mode 100644 index 0000000000..646e18922c --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_serde/cake_serialize.rs @@ -0,0 +1,9 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use serde::Serialize; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Model { + pub id: i32, + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/frontend_with_serde/cake_serialize_with_hidden_column.rs b/sea-orm-codegen/tests/frontend_with_serde/cake_serialize_with_hidden_column.rs new file mode 100644 index 0000000000..a7a1ceb3aa --- /dev/null +++ b/sea-orm-codegen/tests/frontend_with_serde/cake_serialize_with_hidden_column.rs @@ -0,0 +1,10 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use serde::Serialize; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Model { + pub id: i32, + #[serde(skip)] + pub name: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/postgres/binary_json.rs b/sea-orm-codegen/tests/postgres/binary_json.rs index 3d7cf95075..64730c8099 100644 --- a/sea-orm-codegen/tests/postgres/binary_json.rs +++ b/sea-orm-codegen/tests/postgres/binary_json.rs @@ -7,7 +7,7 @@ use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "task")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/postgres/binary_json_expanded.rs b/sea-orm-codegen/tests/postgres/binary_json_expanded.rs index 543e77f1f6..2971d02dde 100644 --- a/sea-orm-codegen/tests/postgres/binary_json_expanded.rs +++ b/sea-orm-codegen/tests/postgres/binary_json_expanded.rs @@ -13,7 +13,7 @@ impl EntityName for Entity { fn schema_name(&self) -> Option< &str > { Some("schema_name") } - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "task" } } diff --git a/sea-orm-codegen/tests/with_seaography/cake.rs b/sea-orm-codegen/tests/with_seaography/cake.rs index 41a3068b30..fda9c38478 100644 --- a/sea-orm-codegen/tests/with_seaography/cake.rs +++ b/sea-orm-codegen/tests/with_seaography/cake.rs @@ -2,7 +2,7 @@ use sea_orm::entity::prelude:: * ; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] diff --git a/sea-orm-codegen/tests/with_seaography/cake_expanded.rs b/sea-orm-codegen/tests/with_seaography/cake_expanded.rs index b4110b37e7..58b2eb8953 100644 --- a/sea-orm-codegen/tests/with_seaography/cake_expanded.rs +++ b/sea-orm-codegen/tests/with_seaography/cake_expanded.rs @@ -6,7 +6,7 @@ use sea_orm::entity::prelude:: * ; pub struct Entity; impl EntityName for Entity { - fn table_name(&self) -> &str { + fn table_name(&self) -> & 'static str { "cake" } } diff --git a/sea-orm-codegen/tests/with_seaography/cake_frontend.rs b/sea-orm-codegen/tests/with_seaography/cake_frontend.rs new file mode 100644 index 0000000000..1d0c4eccee --- /dev/null +++ b/sea-orm-codegen/tests/with_seaography/cake_frontend.rs @@ -0,0 +1,8 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub id: i32, + pub name: Option , + pub base_id: Option , +} \ No newline at end of file diff --git a/sea-orm-codegen/tests/with_seaography/mod.rs b/sea-orm-codegen/tests/with_seaography/mod.rs new file mode 100644 index 0000000000..59445e8d7a --- /dev/null +++ b/sea-orm-codegen/tests/with_seaography/mod.rs @@ -0,0 +1,22 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +seaography::register_entity_modules!([ + cake, + cake_filling, + cake_filling_price, + filling, + fruit, + vendor, + rust_keyword, + cake_with_float, + cake_with_double, + collection, + collection_float, + parent, + child, +]); + +seaography::register_active_enums!([ + sea_orm_active_enums::CoinflipResultType, + sea_orm_active_enums::MediaType, +]); diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index ee4c65b125..2f4dbe744a 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -1,37 +1,52 @@ [package] -name = "sea-orm-macros" -version = "0.12.0" -authors = [ "Billy Chan " ] -edition = "2021" -description = "Derive macros for SeaORM" -license = "MIT OR Apache-2.0" -homepage = "https://www.sea-ql.org/SeaORM" +authors = ["Billy Chan "] +categories = ["database"] +description = "Derive macros for SeaORM" documentation = "https://docs.rs/sea-orm" -repository = "https://github.com/SeaQL/sea-orm" -categories = [ "database" ] -keywords = ["async", "orm", "mysql", "postgres", "sqlite"] -rust-version = "1.65" +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" +keywords = ["async", "orm", "mysql", "postgres", "sqlite"] +license = "MIT OR Apache-2.0" +name = "sea-orm-macros" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.37" [lib] -name = "sea_orm_macros" -path = "src/lib.rs" +name = "sea_orm_macros" +path = "src/lib.rs" proc-macro = true [dependencies] -bae = { version = "0.1", default-features = false, optional = true } -syn = { version = "1", default-features = false, features = ["parsing", "proc-macro", "derive", "printing"] } -quote = { version = "1", default-features = false } -heck = { version = "0.4", default-features = false } +bae = { version = "0.2", package = "sea-bae", default-features = false, optional = true } +heck = { version = "0.5", default-features = false } +itertools = "0.14" +pluralizer = { version = "0.5" } +proc-macro-crate = { version = "3.2.0", optional = true } proc-macro2 = { version = "1", default-features = false } +quote = { version = "1", default-features = false } +syn = { version = "2", default-features = false, features = [ + "parsing", + "proc-macro", + "derive", + "printing", +] } unicode-ident = { version = "1" } [dev-dependencies] -sea-orm = { path = "../", features = ["macros", "tests-cfg"] } +sea-orm = { path = "../", default-features = false, features = [ + "macros", + "tests-cfg", +] } serde = { version = "1.0", features = ["derive"] } [features] -default = ["derive"] -postgres-array = [] -derive = ["bae"] -strum = [] -seaography = [] +async = [] +default = ["derive"] +derive = ["bae"] +entity-registry = [] +postgres-array = [] +seaography = ["proc-macro-crate"] +strum = [] +with-arrow = [] +with-json = [] diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index aa85f8a95e..1049d2c519 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -1,14 +1,9 @@ +use super::case_style::{CaseStyle, CaseStyleHelpers}; use super::util::camel_case_with_escaped_non_uax31; use heck::ToUpperCamelCase; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned}; -use syn::{parse, punctuated::Punctuated, token::Comma, Expr, Lit, LitInt, LitStr, Meta, UnOp}; - -enum Error { - InputNotEnum, - Syn(syn::Error), - TT(TokenStream), -} +use syn::{Expr, Lit, LitInt, LitStr, UnOp, parse}; struct ActiveEnum { ident: syn::Ident, @@ -17,12 +12,20 @@ struct ActiveEnum { db_type: TokenStream, is_string: bool, variants: Vec, + rename_all: Option, } struct ActiveEnumVariant { ident: syn::Ident, string_value: Option, num_value: Option, + rename: Option, +} + +enum Error { + InputNotEnum, + Syn(syn::Error), + TT(TokenStream), } impl ActiveEnum { @@ -37,92 +40,93 @@ impl ActiveEnum { let mut db_type = Err(Error::TT(quote_spanned! { ident_span => compile_error!("Missing macro attribute `db_type`"); })); - for attr in input.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { - continue; - } - if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { - for meta in list.iter() { - if let Meta::NameValue(nv) = meta { - if let Some(name) = nv.path.get_ident() { - if name == "rs_type" { - if let Lit::Str(litstr) = &nv.lit { - rs_type = syn::parse_str::(&litstr.value()) - .map_err(Error::Syn); - } - } else if name == "db_type" { - if let Lit::Str(litstr) = &nv.lit { - let s = litstr.value(); - match s.as_ref() { - "Enum" => { - db_type = Ok(quote! { - Enum { - name: Self::name(), - variants: Self::iden_values(), - } - }) - } - _ => { - db_type = syn::parse_str::(&s) - .map_err(Error::Syn); - } + let mut rename_all = None; + + input + .attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rs_type") { + let litstr: LitStr = meta.value()?.parse()?; + rs_type = + syn::parse_str::(&litstr.value()).map_err(Error::Syn); + } else if meta.path.is_ident("db_type") { + let litstr: LitStr = meta.value()?.parse()?; + let s = litstr.value(); + match s.as_ref() { + "Enum" => { + db_type = Ok(quote! { + Enum { + name: ::name(), + variants: Self::iden_values(), } - } - } else if name == "enum_name" { - if let Lit::Str(litstr) = &nv.lit { - enum_name = litstr.value(); - } + }) + } + _ => { + db_type = syn::parse_str::(&s).map_err(Error::Syn); } } + } else if meta.path.is_ident("enum_name") { + let litstr: LitStr = meta.value()?.parse()?; + enum_name = litstr.value(); + } else if meta.path.is_ident("rename_all") { + rename_all = Some((&meta).try_into()?); + } else { + return Err(meta.error(format!( + "Unknown attribute parameter found: {:?}", + meta.path.get_ident() + ))); } - } - } - } + Ok(()) + }) + .map_err(Error::Syn) + })?; let variant_vec = match input.data { syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, _ => return Err(Error::InputNotEnum), }; - let mut is_string = false; + let mut is_string = rename_all.is_some(); let mut is_int = false; let mut variants = Vec::new(); + for variant in variant_vec { let variant_span = variant.ident.span(); let mut string_value = None; let mut num_value = None; + let mut rename_rule = None; + for attr in variant.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { + if !attr.path().is_ident("sea_orm") { continue; } - if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) - { - for meta in list { - if let Meta::NameValue(nv) = meta { - if let Some(name) = nv.path.get_ident() { - if name == "string_value" { - if let Lit::Str(lit) = nv.lit { - is_string = true; - string_value = Some(lit); - } - } else if name == "num_value" { - if let Lit::Int(lit) = nv.lit { - is_int = true; - num_value = Some(lit); - } - } - } - } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("string_value") { + is_string = true; + string_value = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("num_value") { + is_int = true; + num_value = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("display_value") { + // This is a placeholder to prevent the `display_value` proc_macro attribute of `DeriveDisplay` + // to be considered unknown attribute parameter + meta.value()?.parse::()?; + } else if meta.path.is_ident("rename") { + is_string = true; + rename_rule = Some((&meta).try_into()?); + } else { + return Err(meta.error(format!( + "Unknown attribute parameter found: {:?}", + meta.path.get_ident() + ))); } - } + + Ok(()) + }) + .map_err(Error::Syn)?; } if is_string && is_int { @@ -131,7 +135,8 @@ impl ActiveEnum { })); } - if string_value.is_none() && num_value.is_none() { + if string_value.is_none() && num_value.is_none() && rename_rule.or(rename_all).is_none() + { match variant.discriminant { Some((_, Expr::Lit(exprlit))) => { if let Lit::Int(litint) = exprlit.lit { @@ -163,7 +168,7 @@ impl ActiveEnum { } _ => { return Err(Error::TT(quote_spanned! { - variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified or specify repr[X] and have a value for every entry"); + variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename` should be specified or specify repr[X] and have a value for every entry"); })); } } @@ -173,6 +178,7 @@ impl ActiveEnum { ident: variant.ident, string_value, num_value, + rename: rename_rule, }); } @@ -183,6 +189,7 @@ impl ActiveEnum { db_type: db_type?, is_string, variants, + rename_all, }) } @@ -200,6 +207,7 @@ impl ActiveEnum { db_type, is_string, variants, + rename_all, } = self; let variant_idents: Vec = variants @@ -217,9 +225,12 @@ impl ActiveEnum { quote! { #string } } else if let Some(num_value) = &variant.num_value { quote! { #num_value } + } else if let Some(rename_rule) = variant.rename.or(*rename_all) { + let variant_ident = variant.ident.convert_case(Some(rename_rule)); + quote! { #variant_ident } } else { quote_spanned! { - variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified"); + variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename_all` should be specified"); } } }) @@ -240,6 +251,10 @@ impl ActiveEnum { .string_value .as_ref() .map(|string_value| string_value.value()) + .or(variant + .rename + .map(|rename| variant.ident.convert_case(Some(rename)))) + .or_else(|| rename_all.map(|rule| variant.ident.convert_case(Some(rule)))) }) .collect(); @@ -265,13 +280,13 @@ impl ActiveEnum { } #[automatically_derived] - impl sea_orm::sea_query::Iden for #enum_variant_iden { - fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", match self { + impl sea_orm::Iden for #enum_variant_iden { + fn unquoted(&self) -> &str { + match self { #( Self::#enum_variants => #str_variants, )* - }).unwrap(); + } } } @@ -292,7 +307,23 @@ impl ActiveEnum { let impl_not_u8 = if cfg!(feature = "postgres-array") { quote!( #[automatically_derived] - impl sea_orm::sea_query::value::with_array::NotU8 for #ident {} + impl sea_orm::sea_query::postgres_array::NotU8 for #ident {} + ) + } else { + quote!() + }; + + let impl_try_getable_array = if cfg!(feature = "postgres-array") { + quote!( + #[automatically_derived] + impl sea_orm::TryGetableArray for #ident { + fn try_get_by(res: &sea_orm::QueryResult, index: I) -> std::result::Result, sea_orm::TryGetError> { + <::Value as sea_orm::ActiveEnumValue>::try_get_vec_by(res, index)? + .into_iter() + .map(|value| ::try_from_value(&value).map_err(Into::into)) + .collect() + } + } ) } else { quote!() @@ -304,9 +335,9 @@ impl ActiveEnum { pub struct #enum_name_iden; #[automatically_derived] - impl sea_orm::sea_query::Iden for #enum_name_iden { - fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", #enum_name).unwrap(); + impl sea_orm::Iden for #enum_name_iden { + fn unquoted(&self) -> &str { + #enum_name } } @@ -322,14 +353,14 @@ impl ActiveEnum { sea_orm::sea_query::SeaRc::new(#enum_name_iden) as sea_orm::sea_query::DynIden } - fn to_value(&self) -> Self::Value { + fn to_value(&self) -> ::Value { match self { #( Self::#variant_idents => #variant_values, )* } .to_owned() } - fn try_from_value(v: &Self::Value) -> std::result::Result { + fn try_from_value(v: &::Value) -> std::result::Result { match #val { #( #variant_values => Ok(Self::#variant_idents), )* _ => Err(sea_orm::DbErr::Type(format!( @@ -345,6 +376,8 @@ impl ActiveEnum { } } + #impl_try_getable_array + #[automatically_derived] #[allow(clippy::from_over_into)] impl Into for #ident { @@ -382,6 +415,10 @@ impl ActiveEnum { .to_owned() .into() } + + fn enum_type_name() -> Option<&'static str> { + Some(stringify!(#ident)) + } } #[automatically_derived] @@ -392,10 +429,9 @@ impl ActiveEnum { } #[automatically_derived] - impl std::fmt::Display for #ident { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let v: sea_orm::sea_query::Value = ::to_value(&self).into(); - write!(f, "{}", v) + impl sea_orm::IntoActiveValue<#ident> for #ident { + fn into_active_value(self) -> sea_orm::ActiveValue<#ident> { + sea_orm::ActiveValue::set(self) } } diff --git a/sea-orm-macros/src/derives/active_enum_display.rs b/sea-orm-macros/src/derives/active_enum_display.rs new file mode 100644 index 0000000000..9ccef24349 --- /dev/null +++ b/sea-orm-macros/src/derives/active_enum_display.rs @@ -0,0 +1,108 @@ +use super::case_style::CaseStyle; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote, quote_spanned}; +use syn::{LitInt, LitStr}; + +enum Error { + InputNotEnum, + Syn(syn::Error), +} + +struct Display { + ident: syn::Ident, + variants: Vec, +} + +struct DisplayVariant { + ident: syn::Ident, + display_value: TokenStream, +} + +impl Display { + fn new(input: syn::DeriveInput) -> Result { + let ident = input.ident; + + let variant_vec = match input.data { + syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, + _ => return Err(Error::InputNotEnum), + }; + + let mut variants = Vec::new(); + for variant in variant_vec { + let mut display_value = variant.ident.to_string().to_token_stream(); + + for attr in variant.attrs.iter() { + if !attr.path().is_ident("sea_orm") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("string_value") { + meta.value()?.parse::()?; + } else if meta.path.is_ident("num_value") { + meta.value()?.parse::()?; + } else if meta.path.is_ident("display_value") { + display_value = meta.value()?.parse::()?.to_token_stream(); + } else if meta.path.is_ident("rename") { + CaseStyle::try_from(&meta)?; + } else { + return Err(meta.error(format!( + "Unknown attribute parameter found: {:?}", + meta.path.get_ident() + ))); + } + + Ok(()) + }) + .map_err(Error::Syn)?; + } + variants.push(DisplayVariant { + ident: variant.ident, + display_value, + }); + } + Ok(Display { ident, variants }) + } + + fn expand(&self) -> syn::Result { + let expanded_impl_active_enum_display = self.impl_active_enum_display(); + + Ok(expanded_impl_active_enum_display) + } + + fn impl_active_enum_display(&self) -> TokenStream { + let Self { ident, variants } = self; + + let variant_idents: Vec<_> = variants + .iter() + .map(|variant| variant.ident.clone()) + .collect(); + + let variant_display: Vec<_> = variants + .iter() + .map(|variant| variant.display_value.to_owned()) + .collect(); + + quote!( + #[automatically_derived] + impl std::fmt::Display for #ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + #( Self::#variant_idents => #variant_display, )* + }) + } + } + ) + } +} + +pub fn expand_derive_active_enum_display(input: syn::DeriveInput) -> syn::Result { + let ident_span = input.ident.span(); + + match Display::new(input) { + Ok(model) => model.expand(), + Err(Error::InputNotEnum) => Ok(quote_spanned! { + ident_span => compile_error!("you can only derive EnumDisplay on enums"); + }), + Err(Error::Syn(e)) => Err(e), + } +} diff --git a/sea-orm-macros/src/derives/active_model.rs b/sea-orm-macros/src/derives/active_model.rs index dbea86f1ac..3171bd7954 100644 --- a/sea-orm-macros/src/derives/active_model.rs +++ b/sea-orm-macros/src/derives/active_model.rs @@ -3,184 +3,239 @@ use super::util::{ }; use heck::ToUpperCamelCase; use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote, quote_spanned}; -use syn::{ - punctuated::{IntoIter, Punctuated}, - token::Comma, - Data, DataStruct, Field, Fields, Lit, Meta, Type, -}; - -/// Method to derive an [ActiveModel](sea_orm::ActiveModel) -pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result { - // including ignored fields - let all_fields = match data { - Data::Struct(DataStruct { - fields: Fields::Named(named), - .. - }) => named.named, - _ => { - return Ok(quote_spanned! { - ident.span() => compile_error!("you can only derive DeriveActiveModel on structs"); - }) - } - } - .into_iter(); +use quote::{format_ident, quote}; +use syn::{Data, DataStruct, Expr, Fields, LitStr, Type}; - let derive_active_model = derive_active_model(all_fields.clone())?; - let derive_into_model = derive_into_model(all_fields)?; - - Ok(quote!( - #derive_active_model - #derive_into_model - )) +pub(crate) struct DeriveActiveModel { + model: Ident, + fields: Vec, + names: Vec, + types: Vec, } -fn derive_active_model(all_fields: IntoIter) -> syn::Result { - let fields = all_fields.filter(field_not_ignored); +impl DeriveActiveModel { + pub fn new(ident: &Ident, data: &Data) -> syn::Result { + let all_fields = match data { + Data::Struct(DataStruct { + fields: Fields::Named(named), + .. + }) => &named.named, + _ => { + return Err(syn::Error::new_spanned( + ident, + "You can only derive DeriveActiveModel on structs", + )); + } + }; - let field: Vec = fields.clone().map(format_field_ident).collect(); + let mut fields = Vec::new(); + let mut names = Vec::new(); + let mut types = Vec::new(); + + for field in all_fields.iter().filter(|f| field_not_ignored(f)) { + fields.push(format_field_ident(field)); - let name: Vec = fields - .clone() - .map(|field| { let ident = field.ident.as_ref().unwrap().to_string(); let ident = trim_starting_raw_identifier(ident).to_upper_camel_case(); let ident = escape_rust_keyword(ident); let mut ident = format_ident!("{}", &ident); - for attr in field.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { - continue; - } - if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) - { - for meta in list.iter() { - if let Meta::NameValue(nv) = meta { - if let Some(name) = nv.path.get_ident() { - if name == "enum_name" { - if let Lit::Str(litstr) = &nv.lit { - ident = syn::parse_str(&litstr.value()).unwrap(); - } - } - } + field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("enum_name") { + let litstr: LitStr = meta.value()?.parse()?; + ident = syn::parse_str(&litstr.value()).unwrap(); + } else { + // Reads the value expression to advance the parse stream. + // Some parameters, such as `primary_key`, do not have any value, + // so ignoring an error occurred here. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); } - } - } - } - ident - }) - .collect(); - let ty: Vec = fields.into_iter().map(|Field { ty, .. }| ty).collect(); + Ok(()) + }) + })?; - Ok(quote!( - #[doc = " Generated by sea-orm-macros"] - #[derive(Clone, Debug, PartialEq)] - pub struct ActiveModel { - - #( - #[doc = " Generated by sea-orm-macros"] - pub #field: sea_orm::ActiveValue<#ty> - ),* + names.push(ident); + types.push(field.ty.clone()); } - #[automatically_derived] - impl std::default::Default for ActiveModel { - fn default() -> Self { - ::new() + Ok(DeriveActiveModel { + model: ident.clone(), + fields, + names, + types, + }) + } +} + +impl DeriveActiveModel { + fn define_active_model(&self) -> TokenStream { + let fields = &self.fields; + let types = &self.types; + quote!( + #[doc = " Generated by sea-orm-macros"] + #[derive(Clone, Debug, PartialEq)] + pub struct ActiveModel { + #( + #[doc = " Generated by sea-orm-macros"] + pub #fields: sea_orm::ActiveValue<#types> + ),* } - } + ) + } - #[automatically_derived] - impl std::convert::From<::Model> for ActiveModel { - fn from(m: ::Model) -> Self { - Self { - #(#field: sea_orm::ActiveValue::unchanged(m.#field)),* + fn impl_active_model(&self) -> TokenStream { + let mut ts = self.impl_active_model_convert(); + ts.extend(self.impl_active_model_trait()); + ts + } + + fn impl_active_model_convert(&self) -> TokenStream { + let model = &self.model; + let fields = &self.fields; + + quote!( + #[automatically_derived] + impl std::default::Default for ActiveModel { + fn default() -> Self { + ::new() } } - } - #[automatically_derived] - impl sea_orm::IntoActiveModel for ::Model { - fn into_active_model(self) -> ActiveModel { - self.into() + #[automatically_derived] + impl std::convert::From<#model> for ActiveModel { + fn from(m: #model) -> Self { + Self { + #(#fields: sea_orm::ActiveValue::Unchanged(m.#fields)),* + } + } + } + + #[automatically_derived] + impl sea_orm::IntoActiveModel for #model { + fn into_active_model(self) -> ActiveModel { + self.into() + } + } + ) + } + + fn impl_active_model_trait(&self) -> TokenStream { + let fields = &self.fields; + let methods = self.impl_active_model_trait_methods(); + + quote! { + #[automatically_derived] + impl sea_orm::ActiveModelTrait for ActiveModel { + type Entity = Entity; + + #methods + + fn default() -> Self { + Self { + #(#fields: sea_orm::ActiveValue::NotSet),* + } + } } } + } - #[automatically_derived] - impl sea_orm::ActiveModelTrait for ActiveModel { - type Entity = Entity; + pub fn impl_active_model_trait_methods(&self) -> TokenStream { + let fields = &self.fields; + let names = &self.names; - fn take(&mut self, c: ::Column) -> sea_orm::ActiveValue { + quote!( + fn take(&mut self, c: ::Column) -> sea_orm::ActiveValue { match c { - #(::Column::#name => { - let mut value = sea_orm::ActiveValue::not_set(); - std::mem::swap(&mut value, &mut self.#field); + #(::Column::#names => { + let mut value = sea_orm::ActiveValue::NotSet; + std::mem::swap(&mut value, &mut self.#fields); value.into_wrapped_value() },)* - _ => sea_orm::ActiveValue::not_set(), + _ => sea_orm::ActiveValue::NotSet, } } - fn get(&self, c: ::Column) -> sea_orm::ActiveValue { + fn get(&self, c: ::Column) -> sea_orm::ActiveValue { match c { - #(::Column::#name => self.#field.clone().into_wrapped_value(),)* - _ => sea_orm::ActiveValue::not_set(), + #(::Column::#names => self.#fields.clone().into_wrapped_value(),)* + _ => sea_orm::ActiveValue::NotSet, } } - fn set(&mut self, c: ::Column, v: sea_orm::Value) { + fn set_if_not_equals(&mut self, c: ::Column, v: sea_orm::Value) { match c { - #(::Column::#name => self.#field = sea_orm::ActiveValue::set(v.unwrap()),)* - _ => panic!("This ActiveModel does not have this field"), + #(::Column::#names => self.#fields.set_if_not_equals(v.unwrap()),)* + _ => (), } } - fn not_set(&mut self, c: ::Column) { + fn try_set(&mut self, c: ::Column, v: sea_orm::Value) -> Result<(), sea_orm::DbErr> { match c { - #(::Column::#name => self.#field = sea_orm::ActiveValue::not_set(),)* - _ => {}, + #(::Column::#names => self.#fields = sea_orm::ActiveValue::Set(sea_orm::sea_query::ValueType::try_from(v).map_err(|e| sea_orm::DbErr::Type(e.to_string()))?),)* + _ => return Err(sea_orm::DbErr::Type(format!("ActiveModel does not have this field: {:?}", sea_orm::ColumnTrait::as_column_ref(&c)))), } + Ok(()) } - fn is_not_set(&self, c: ::Column) -> bool { + fn not_set(&mut self, c: ::Column) { match c { - #(::Column::#name => self.#field.is_not_set(),)* - _ => panic!("This ActiveModel does not have this field"), + #(::Column::#names => self.#fields = sea_orm::ActiveValue::NotSet,)* + _ => (), } } - fn default() -> Self { - Self { - #(#field: sea_orm::ActiveValue::not_set()),* + fn is_not_set(&self, c: ::Column) -> bool { + match c { + #(::Column::#names => self.#fields.is_not_set(),)* + _ => panic!("This ActiveModel does not have this field"), } } - fn reset(&mut self, c: ::Column) { + fn reset(&mut self, c: ::Column) { match c { - #(::Column::#name => self.#field.reset(),)* + #(::Column::#names => self.#fields.reset(),)* _ => panic!("This ActiveModel does not have this field"), } } - } - )) + + fn default_values() -> Self { + use sea_orm::value::{DefaultActiveValue, DefaultActiveValueNone, DefaultActiveValueNotSet}; + let mut default = ::default(); + #(default.#fields = (&default.#fields).default_value();)* + default + } + ) + } } -fn derive_into_model(model_fields: IntoIter) -> syn::Result { - let active_model_fields = model_fields.clone().filter(field_not_ignored); +fn derive_into_model(ident: &Ident, data: &Data) -> syn::Result { + let model_fields = match data { + Data::Struct(DataStruct { + fields: Fields::Named(named), + .. + }) => &named.named, + _ => { + return Err(syn::Error::new_spanned( + ident, + "You can only derive DeriveActiveModel on structs", + )); + } + }; - let active_model_field: Vec = active_model_fields - .into_iter() + let active_model_field: Vec = model_fields + .iter() + .filter(|f| field_not_ignored(f)) .map(format_field_ident) .collect(); - let model_field: Vec = model_fields.clone().map(format_field_ident).collect(); - let ignore_attr: Vec = model_fields - .map(|field| !field_not_ignored(&field)) - .collect(); + let model_field: Vec = model_fields.iter().map(format_field_ident).collect(); + + let ignore_attr: Vec = model_fields.iter().map(|f| !field_not_ignored(f)).collect(); let model_field_value: Vec = model_field .iter() @@ -192,7 +247,7 @@ fn derive_into_model(model_fields: IntoIter) -> syn::Result } } else { quote! { - a.#field.into_value().unwrap().unwrap() + a.#field.unwrap() } } }) @@ -200,10 +255,10 @@ fn derive_into_model(model_fields: IntoIter) -> syn::Result Ok(quote!( #[automatically_derived] - impl std::convert::TryFrom for ::Model { + impl std::convert::TryFrom for #ident { type Error = sea_orm::DbErr; fn try_from(a: ActiveModel) -> Result { - #(if matches!(a.#active_model_field, sea_orm::ActiveValue::NotSet) { + #(if a.#active_model_field.is_not_set() { return Err(sea_orm::DbErr::AttrNotSet(stringify!(#active_model_field).to_owned())); })* Ok( @@ -215,10 +270,26 @@ fn derive_into_model(model_fields: IntoIter) -> syn::Result } #[automatically_derived] - impl sea_orm::TryIntoModel<::Model> for ActiveModel { - fn try_into_model(self) -> Result<::Model, sea_orm::DbErr> { + impl sea_orm::TryIntoModel<#ident> for ActiveModel { + fn try_into_model(self) -> Result<#ident, sea_orm::DbErr> { self.try_into() } } )) } + +pub fn expand_derive_active_model(ident: &Ident, data: &Data) -> syn::Result { + let derive_active_model = DeriveActiveModel::new(ident, data)?; + + let define_active_model = derive_active_model.define_active_model(); + let impl_active_model = derive_active_model.impl_active_model(); + let derive_into_model = derive_into_model(ident, data)?; + + Ok(quote!( + #define_active_model + + #impl_active_model + + #derive_into_model + )) +} diff --git a/sea-orm-macros/src/derives/active_model_ex.rs b/sea-orm-macros/src/derives/active_model_ex.rs new file mode 100644 index 0000000000..f9ff25ad4d --- /dev/null +++ b/sea-orm-macros/src/derives/active_model_ex.rs @@ -0,0 +1,789 @@ +use super::active_model::DeriveActiveModel; +use super::attributes::compound_attr; +use super::util::{extract_compound_entity, field_not_ignored_compound, is_compound_field}; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use std::collections::HashMap; +use syn::{Attribute, Data, Expr, Fields, LitStr, Type}; + +pub fn expand_derive_active_model_ex( + ident: &Ident, + data: &Data, + attrs: &[Attribute], +) -> syn::Result { + let mut compact = false; + let mut model_fields = Vec::new(); + let mut ignored_model_fields = Vec::new(); + let mut field_types: Vec = Vec::new(); + let mut scalar_fields = Vec::new(); + let mut compound_fields = Vec::new(); + let mut belongs_to_fields = Vec::new(); + let mut belongs_to_self_fields = Vec::new(); + let mut has_one_fields = Vec::new(); + let mut has_many_fields = Vec::new(); + let mut has_many_self_fields = Vec::new(); + let mut has_many_via_fields = Vec::new(); + let mut has_many_via_self_fields = Vec::new(); + + let (async_, await_) = async_await(); + + attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("compact_model") { + compact = true; + } else { + // Reads the value expression to advance the parse stream. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + Ok(()) + }) + })?; + + let mut entity_count = HashMap::new(); + + if let Data::Struct(item_struct) = &data { + if let Fields::Named(fields) = &item_struct.fields { + for field in fields.named.iter() { + if field.ident.is_some() && field_not_ignored_compound(field) { + let field_type = &field.ty; + let field_type = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + if is_compound_field(&field_type) { + let entity_path = extract_compound_entity(&field_type); + *entity_count.entry(entity_path.to_owned()).or_insert(0) += 1; + } + } + } + } + } + + if let Data::Struct(item_struct) = &data { + if let Fields::Named(fields) = &item_struct.fields { + for field in fields.named.iter() { + if let Some(ident) = &field.ident { + if field_not_ignored_compound(field) { + let field_type = &field.ty; + let field_type = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + let ty = if is_compound_field(&field_type) { + compound_fields.push(ident); + let entity_path = extract_compound_entity(&field_type); + + if !compact { + let compound_attrs = + compound_attr::SeaOrm::from_attributes(&field.attrs)?; + if let Some(relation_enum) = compound_attrs.relation_enum { + if compound_attrs.self_ref.is_some() + && compound_attrs.via.is_none() + && compound_attrs.from.is_some() + && compound_attrs.to.is_some() + { + belongs_to_self_fields + .push((ident.clone(), relation_enum.clone())); + } else if compound_attrs.self_ref.is_some() + && compound_attrs.relation_reverse.is_some() + && compound_attrs.via.is_none() + && compound_attrs.from.is_none() + && compound_attrs.to.is_none() + { + has_many_self_fields + .push((ident.clone(), relation_enum.clone())); + } + } else if *entity_count.get(entity_path).unwrap() == 1 { + // can only Related to another Entity once + if compound_attrs.belongs_to.is_some() { + belongs_to_fields.push(ident.clone()); + } else if compound_attrs.has_one.is_some() { + has_one_fields.push(ident.clone()); + } else if compound_attrs.has_many.is_some() + && compound_attrs.via.is_none() + { + has_many_fields.push(ident.clone()); + } else if compound_attrs.has_many.is_some() + && compound_attrs.via.is_some() + { + has_many_via_fields.push(( + ident.clone(), + compound_attrs.via.as_ref().unwrap().value(), + )); + } + } + if compound_attrs.self_ref.is_some() + && compound_attrs.via.is_some() + && compound_attrs.reverse.is_none() + { + #[allow(clippy::unnecessary_unwrap)] + has_many_via_self_fields.push(( + ident.clone(), + compound_attrs.via.as_ref().unwrap().value(), + false, + )); + } else if compound_attrs.self_ref.is_some() + && compound_attrs.via.is_some() + && compound_attrs.reverse.is_some() + { + #[allow(clippy::unnecessary_unwrap)] + has_many_via_self_fields.push(( + ident.clone(), + compound_attrs.via.as_ref().unwrap().value(), + true, + )); + } + } + + if field_type.starts_with("HasOne<") { + syn::parse_str(&format!("HasOneModel < {entity_path} >"))? + } else { + syn::parse_str(&format!("HasManyModel < {entity_path} >"))? + } + } else { + scalar_fields.push(ident); + syn::parse_str(&format!("sea_orm::ActiveValue < {field_type} >"))? + }; + model_fields.push(ident); + field_types.push(ty); + } else { + ignored_model_fields.push(ident); + } + } + } + } + } + + let active_model_trait_methods = + DeriveActiveModel::new(ident, data)?.impl_active_model_trait_methods(); + + let active_model_action = expand_active_model_action( + &belongs_to_fields, + &belongs_to_self_fields, + &has_one_fields, + &has_many_fields, + &has_many_self_fields, + &has_many_via_fields, + &has_many_via_self_fields, + ); + + let active_model_setters = expand_active_model_setters(data)?; + + let mut is_changed_expr = quote!(false); + + for field in scalar_fields.iter() { + is_changed_expr.extend(quote!(|| self.#field.is_set())); + } + for field in compound_fields.iter() { + is_changed_expr.extend(quote!(|| self.#field.is_changed())); + } + + Ok(quote! { + #[doc = " Generated by sea-orm-macros"] + #[derive(Clone, Debug, PartialEq)] + pub struct ActiveModelEx { + #( + #[doc = " Generated by sea-orm-macros"] + pub #model_fields: #field_types + ),* + } + + impl ActiveModel { + #[doc = " Generated by sea-orm-macros"] + pub fn into_ex(self) -> ActiveModelEx { + self.into() + } + } + + #[automatically_derived] + impl sea_orm::ActiveModelTrait for ActiveModelEx { + type Entity = Entity; + + #active_model_trait_methods + + /// Returns true if any field is set or changed. This is recursive. + fn is_changed(&self) -> bool { + #is_changed_expr + } + + fn default() -> Self { + ::new().into() + } + } + + impl ActiveModelEx { + #[doc = " Generated by sea-orm-macros"] + pub fn new() -> Self { + ::default() + } + + #active_model_action + + #active_model_setters + } + + #[automatically_derived] + impl std::default::Default for ActiveModelEx { + fn default() -> Self { + ::default() + } + } + + #[automatically_derived] + impl std::convert::From for ActiveModelEx { + fn from(m: ActiveModel) -> Self { + Self { + #(#scalar_fields: m.#scalar_fields,)* + #(#compound_fields: Default::default(),)* + } + } + } + + #[automatically_derived] + impl std::convert::From for ActiveModel { + fn from(m: ActiveModelEx) -> Self { + Self { + #(#scalar_fields: m.#scalar_fields,)* + } + } + } + + #[automatically_derived] + impl std::convert::From for ActiveModelEx { + fn from(m: ModelEx) -> Self { + Self { + #(#scalar_fields: sea_orm::ActiveValue::Unchanged(m.#scalar_fields),)* + #(#compound_fields: m.#compound_fields.into_active_model(),)* + } + } + } + + #[automatically_derived] + impl std::convert::TryFrom for ModelEx { + type Error = sea_orm::DbErr; + fn try_from(a: ActiveModelEx) -> Result { + #(if a.#scalar_fields.is_not_set() { + return Err(sea_orm::DbErr::AttrNotSet(stringify!(#scalar_fields).to_owned())); + })* + Ok( + Self { + #(#scalar_fields: a.#scalar_fields.unwrap(),)* + #(#compound_fields: a.#compound_fields.try_into_model()?,)* + #(#ignored_model_fields: Default::default(),)* + } + ) + } + } + + #[automatically_derived] + impl sea_orm::IntoActiveModel for ModelEx { + fn into_active_model(self) -> ActiveModelEx { + self.into() + } + } + + #[automatically_derived] + impl sea_orm::TryIntoModel for ActiveModelEx { + fn try_into_model(self) -> Result { + self.try_into() + } + } + + impl Model { + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn cascade_delete<'a, C>(self, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + self.into_ex().delete(db)#await_ + } + } + + impl ModelEx { + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn delete<'a, C>(self, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + let active_model: ActiveModelEx = self.into(); + active_model.delete(db)#await_ + } + } + + impl ActiveModel { + #[doc = " Generated by sea-orm-macros"] + pub fn builder() -> ActiveModelEx { + ActiveModelEx::new() + } + } + }) +} + +fn expand_active_model_action( + belongs_to: &[Ident], + belongs_to_self: &[(Ident, LitStr)], + has_one: &[Ident], + has_many: &[Ident], + has_many_self: &[(Ident, LitStr)], + has_many_via: &[(Ident, String)], + has_many_via_self: &[(Ident, String, bool)], +) -> TokenStream { + let mut belongs_to_action = TokenStream::new(); + let mut belongs_to_after_action = TokenStream::new(); + let mut has_one_before_action = TokenStream::new(); + let mut has_one_action = TokenStream::new(); + let mut has_one_delete = TokenStream::new(); + let mut has_many_before_action = TokenStream::new(); + let mut has_many_action = TokenStream::new(); + let mut has_many_delete = TokenStream::new(); + let mut has_many_via_before_action = TokenStream::new(); + let mut has_many_via_action = TokenStream::new(); + let mut has_many_via_delete = TokenStream::new(); + + let (async_, await_) = async_await(); + let box_pin = if cfg!(feature = "async") { + quote!(Box::pin) + } else { + quote!() + }; + + for field in belongs_to { + belongs_to_action.extend(quote! { + let #field = if let Some(model) = self.#field.take() { + if model.is_update() { + // has primary key + self.set_parent_key(&model)?; + if model.is_changed() { + let model = #box_pin(model.action(action, db))#await_?; + Some(model) + } else { + Some(model) + } + } else { + // new model + let model = #box_pin(model.action(action, db))#await_?; + self.set_parent_key(&model)?; + Some(model) + } + } else { + None + }; + }); + + belongs_to_after_action.extend(quote! { + if let Some(#field) = #field { + model.#field = HasOneModel::set(#field); + } + }); + } + + for (field, relation_enum) in belongs_to_self { + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + let relation_enum = quote!(Relation::#relation_enum); + + // belongs to is the exception where action is performed before self + belongs_to_action.extend(quote! { + let #field = if let Some(model) = self.#field.take() { + if model.is_update() { + // has primary key + self.set_parent_key_for(&model, #relation_enum)?; + if model.is_changed() { + let model = #box_pin(model.action(action, db))#await_?; + Some(model) + } else { + Some(model) + } + } else { + // new model + let model = #box_pin(model.action(action, db))#await_?; + self.set_parent_key_for(&model, #relation_enum)?; + Some(model) + } + } else { + None + }; + }); + + belongs_to_after_action.extend(quote! { + if let Some(#field) = #field { + model.#field = HasOneModel::set(#field); + } + }); + } + + let delete_associated_model = quote! { + let mut item = item.into_active_model(); + if item.clear_parent_key::()? { + item.update(db)#await_?; + } else { + deleted.merge(item.into_ex().delete(db)#await_?); // deep delete + } + }; + + for field in has_one { + has_one_before_action.extend(quote! { + let #field = self.#field.take(); + }); + + has_one_action.extend(quote! { + if let Some(mut #field) = #field { + #field.set_parent_key(&model)?; + if #field.is_changed() { + model.#field = HasOneModel::set(#box_pin(#field.action(action, db))#await_?); + } else { + model.#field = HasOneModel::set(#field); + } + } + }); + + has_one_delete.extend(quote! { + if let Some(item) = self.find_related_of(self.#field.empty_slice()).one(db)#await_? { + #delete_associated_model + } + }); + } + + for field in has_many { + has_many_before_action.extend(quote! { + let #field = self.#field.take(); + }); + + has_many_action.extend(quote! { + if #field.is_replace() { + for item in model.find_related_of(#field.as_slice()).all(db)#await_? { + if !#field.find(&item) { + #delete_associated_model + } + } + } + model.#field = #field.empty_holder(); + for mut #field in #field.into_vec() { + #field.set_parent_key(&model)?; + if #field.is_changed() { + model.#field.push(#box_pin(#field.action(action, db))#await_?); + } else { + model.#field.push(#field); + } + } + }); + + has_many_delete.extend(quote! { + for item in self.find_related_of(self.#field.as_slice()).all(db)#await_? { + #delete_associated_model + } + }); + } + + for (field, relation_enum) in has_many_self { + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + let relation_enum = quote!(Relation::#relation_enum); + + let delete_associated_model = quote! { + let mut item = item.into_active_model(); + if item.clear_parent_key_for_self_rev(#relation_enum)? { + item.update(db)#await_?; + } else { + // attempt to cascade delete may lead to infinite recursion + return Err(sea_orm::DbErr::RecordNotUpdated); + } + }; + + has_many_before_action.extend(quote! { + let #field = self.#field.take(); + }); + + has_many_action.extend(quote! { + if #field.is_replace() { + for item in model.find_belongs_to_self(#relation_enum, db.get_database_backend())?.all(db)#await_? { + if !#field.find(&item) { + #delete_associated_model + } + } + } + model.#field = #field.empty_holder(); + for mut #field in #field.into_vec() { + #field.set_parent_key_for_self_rev(&model, #relation_enum)?; + if #field.is_changed() { + model.#field.push(#box_pin(#field.action(action, db))#await_?); + } else { + model.#field.push(#field); + } + } + }); + + has_many_delete.extend(quote! { + for item in self.find_belongs_to_self(#relation_enum, db.get_database_backend())?.all(db)#await_? { + #delete_associated_model + } + }); + } + + for (field, via_entity) in has_many_via { + let mut via_entity = via_entity.as_str(); + if let Some((prefix, _)) = via_entity.split_once("::") { + via_entity = prefix; + } + + let related_entity: TokenStream = format!("super::{via_entity}::Entity").parse().unwrap(); + + has_many_via_before_action.extend(quote! { + let #field = self.#field.take(); + }); + + has_many_via_action.extend(quote! { + model.#field = #field.empty_holder(); + for item in #field.into_vec() { + if item.is_update() { + // has primary key + if item.is_changed() { + model.#field.push(#box_pin(item.action(action, db))#await_?); + } else { + model.#field.push(item); + } + } else { + // new model + model.#field.push(#box_pin(item.action(action, db))#await_?); + } + } + model.establish_links( + #related_entity, + model.#field.as_slice(), + model.#field.is_replace(), + db + )#await_?; + }); + + has_many_via_delete.extend(quote! { + deleted.merge(self.delete_links(#related_entity, db)#await_?); + }); + } + + for (field, via_entity, reverse) in has_many_via_self { + let related_entity: TokenStream = format!("super::{via_entity}::Entity").parse().unwrap(); + let establish_links = Ident::new( + if *reverse { + "establish_links_self_rev" + } else { + "establish_links_self" + }, + field.span(), + ); + + has_many_via_before_action.extend(quote! { + let #field = self.#field.take(); + }); + + has_many_via_action.extend(quote! { + model.#field = #field.empty_holder(); + for item in #field.into_vec() { + if item.is_update() { + // has primary key + if item.is_changed() { + model.#field.push(#box_pin(item.action(action, db))#await_?); + } else { + model.#field.push(item); + } + } else { + // new model + model.#field.push(#box_pin(item.action(action, db))#await_?); + } + } + model.#establish_links( + #related_entity, + model.#field.as_slice(), + model.#field.is_replace(), + db + )#await_?; + }); + + has_many_via_delete.extend(quote! { + deleted.merge(self.delete_links_self(#related_entity, db)#await_?); + }); + } + + quote! { + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn insert<'a, C>(self, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + let active_model = self.action(sea_orm::ActiveModelAction::Insert, db)#await_?; + active_model.try_into() + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn update<'a, C>(self, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + let active_model = self.action(sea_orm::ActiveModelAction::Update, db)#await_?; + active_model.try_into() + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn save<'a, C>(self, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + self.action(sea_orm::ActiveModelAction::Save, db)#await_ + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn delete<'a, C>(self, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + use sea_orm::{IntoActiveModel, TransactionSession}; + + let txn = db.begin()#await_?; + let db = &txn; + let mut deleted = sea_orm::DeleteResult::empty(); + + #has_one_delete + #has_many_delete + #has_many_via_delete + + let model: ActiveModel = self.into(); + deleted.merge(model.delete(db)#await_?); + + txn.commit()#await_?; + + Ok(deleted) + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn action<'a, C>(mut self, action: sea_orm::ActiveModelAction, db: &'a C) -> Result + where + C: sea_orm::TransactionTrait, + { + use sea_orm::{HasOneModel, HasManyModel, IntoActiveModel, TransactionSession}; + let txn = db.begin()#await_?; + let db = &txn; + let mut deleted = sea_orm::DeleteResult::empty(); + + #belongs_to_action + #has_one_before_action + #has_many_before_action + #has_many_via_before_action + + let model: ActiveModel = self.into(); + + let mut model: Self = if model.is_changed() { + match action { + sea_orm::ActiveModelAction::Insert => model.insert(db)#await_, + sea_orm::ActiveModelAction::Update => model.update(db)#await_, + sea_orm::ActiveModelAction::Save => if !model.is_update() { + model.insert(db)#await_ + } else { + model.update(db)#await_ + }, + }?.into_ex().into() + } else { + model.into() + }; + + #belongs_to_after_action + #has_one_action + #has_many_action + #has_many_via_action + + txn.commit()#await_?; + + Ok(model) + } + } +} + +fn expand_active_model_setters(data: &Data) -> syn::Result { + let mut setters = TokenStream::new(); + + if let Data::Struct(item_struct) = &data { + if let Fields::Named(fields) = &item_struct.fields { + for field in &fields.named { + if let Some(ident) = &field.ident { + let field_type = &field.ty; + let field_type_str = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + let mut ignore = false; + + for attr in field.attrs.iter() { + if attr.path().is_ident("sea_orm") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("ignore") { + ignore = true; + } else { + // Reads the value expression to advance the parse stream. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + + Ok(()) + })?; + } + } + + if ignore { + continue; + } + + if is_compound_field(&field_type_str) { + let entity_path = extract_compound_entity(&field_type_str); + let active_model_type: Type = syn::parse_str(&format!( + "{}ActiveModelEx", + entity_path.trim_end_matches("Entity") + ))?; + + if field_type_str.starts_with("HasOne<") { + let setter = format_ident!("set_{}", ident); + + setters.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #setter(mut self, v: impl Into<#active_model_type>) -> Self { + self.#ident.replace(v.into()); + self + } + }); + } else { + let setter = format_ident!( + "add_{}", + pluralizer::pluralize(&ident.to_string(), 1, false) + ); + + setters.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #setter(mut self, v: impl Into<#active_model_type>) -> Self { + self.#ident.push(v.into()); + self + } + }); + } + } else { + let setter = format_ident!("set_{}", ident); + + setters.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #setter(mut self, v: impl Into<#field_type>) -> Self { + self.#ident = sea_orm::Set(v.into()); + self + } + }); + } + } + } + } + } + + Ok(setters) +} + +fn async_await() -> (TokenStream, TokenStream) { + if cfg!(feature = "async") { + (quote!(async), quote!(.await)) + } else { + (quote!(), quote!()) + } +} diff --git a/sea-orm-macros/src/derives/arrow_schema.rs b/sea-orm-macros/src/derives/arrow_schema.rs new file mode 100644 index 0000000000..bf825985b7 --- /dev/null +++ b/sea-orm-macros/src/derives/arrow_schema.rs @@ -0,0 +1,362 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use syn::{Attribute, Data, Fields, LitInt, LitStr, Type}; + +/// Expand the DeriveArrowSchema derive macro +pub fn expand_derive_arrow_schema( + _ident: Ident, + data: Data, + _attrs: Vec, +) -> syn::Result { + if !cfg!(feature = "with-arrow") { + return Ok(quote!()); + } + + let mut fields_info = Vec::new(); + + // Parse fields + if let Data::Struct(item_struct) = data { + if let Fields::Named(fields) = &item_struct.fields { + for field in &fields.named { + if let Some(field_ident) = &field.ident { + let field_name = field_ident.to_string(); + let field_type = &field.ty; + + // Detect if field is Option for nullability + let type_string = quote! { #field_type }.to_string().replace(' ', ""); + let is_nullable = type_string.starts_with("Option<"); + + // Parse field attributes + let mut arrow_attrs = ArrowFieldAttrs::default(); + let mut column_type_str: Option = None; + let mut column_name_override: Option = None; + let mut arrow_field_override: Option = None; + let mut skip = false; + + for attr in field.attrs.iter() { + if attr.path().is_ident("sea_orm") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("arrow_skip") { + skip = true; + } else if meta.path.is_ident("arrow_field") { + let lit: LitStr = meta.value()?.parse()?; + arrow_field_override = Some(lit.value()); + } else if meta.path.is_ident("column_name") { + let lit: LitStr = meta.value()?.parse()?; + column_name_override = Some(lit.value()); + } else if meta.path.is_ident("arrow_precision") { + let lit: LitInt = meta.value()?.parse()?; + arrow_attrs.precision = Some(lit.base10_parse()?); + } else if meta.path.is_ident("arrow_scale") { + let lit: LitInt = meta.value()?.parse()?; + arrow_attrs.scale = Some(lit.base10_parse()?); + } else if meta.path.is_ident("arrow_timestamp_unit") { + let lit: LitStr = meta.value()?.parse()?; + arrow_attrs.timestamp_unit = Some(lit.value()); + } else if meta.path.is_ident("arrow_timezone") { + let lit: LitStr = meta.value()?.parse()?; + arrow_attrs.timezone = Some(lit.value()); + } else if meta.path.is_ident("arrow_comment") { + let lit: LitStr = meta.value()?.parse()?; + arrow_attrs.comment = Some(lit.value()); + } else if meta.path.is_ident("arrow_byte_width") { + let lit: LitInt = meta.value()?.parse()?; + arrow_attrs.byte_width = Some(lit.base10_parse()?); + } else if meta.path.is_ident("column_type") { + let lit: LitStr = meta.value()?.parse()?; + column_type_str = Some(lit.value()); + } else if meta.path.is_ident("nullable") { + arrow_attrs.nullable_attr = true; + } else { + let _ = meta.value().and_then(|v| v.parse::()); + } + Ok(()) + })?; + } + } + + if skip { + continue; // Skip this field + } + + // Determine final nullability + let nullable = is_nullable || arrow_attrs.nullable_attr; + + // Priority: arrow_field > column_name > Rust field name + let resolved_name = arrow_field_override + .or(column_name_override) + .unwrap_or(field_name); + + fields_info.push(ArrowFieldInfo { + name: resolved_name, + field_type: field_type.clone(), + column_type_str, + nullable, + arrow_attrs, + }); + } + } + } + } + + // Generate arrow_schema() method + let field_definitions = fields_info.iter().map(generate_field_definition); + + let entity_name = format_ident!("Entity"); + + Ok(quote! { + #[automatically_derived] + impl sea_orm::ArrowSchema for #entity_name { + fn arrow_schema() -> sea_orm::arrow::datatypes::Schema { + use sea_orm::arrow::datatypes::{DataType, Field, Schema, TimeUnit}; + + Schema::new(vec![ + #(#field_definitions),* + ]) + } + } + }) +} + +#[derive(Default)] +struct ArrowFieldAttrs { + precision: Option, + scale: Option, + timestamp_unit: Option, + timezone: Option, + comment: Option, + nullable_attr: bool, + byte_width: Option, +} + +struct ArrowFieldInfo { + name: String, + field_type: Type, + column_type_str: Option, + #[allow(dead_code)] + nullable: bool, + arrow_attrs: ArrowFieldAttrs, +} + +fn generate_field_definition(info: &ArrowFieldInfo) -> TokenStream { + let field_name = &info.name; + let nullable = true; // we use ActiveModel, where fields can be NotSet. + + // Generate DataType based on column_type or field type + let data_type = if let Some(col_type_str) = &info.column_type_str { + column_type_to_arrow_datatype(col_type_str, &info.arrow_attrs) + } else { + rust_type_to_arrow_datatype(&info.field_type, &info.arrow_attrs) + }; + + // Add metadata if comment is present + if let Some(comment) = &info.arrow_attrs.comment { + quote! { + Field::new(#field_name, #data_type, #nullable) + .with_metadata([( + "comment".into(), + #comment.into() + )].into()) + } + } else { + quote! { + Field::new(#field_name, #data_type, #nullable) + } + } +} + +/// Map SeaORM ColumnType string to Arrow DataType +fn column_type_to_arrow_datatype(col_type: &str, arrow_attrs: &ArrowFieldAttrs) -> TokenStream { + // Parse ColumnType variants + if col_type.starts_with("Decimal(") { + // Extract precision and scale from Decimal(Some((p, s))) + let (precision, scale) = if col_type.contains("Some((") { + // Parse "Decimal(Some((20, 4)))" + if let Some(inner) = col_type + .strip_prefix("Decimal(Some((") + .and_then(|s| s.strip_suffix(")))")) + { + let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect(); + if parts.len() == 2 { + let p = parts[0].parse().unwrap_or(38); + let s = parts[1].parse().unwrap_or(10); + (p, s) + } else { + (38, 10) + } + } else { + (38, 10) + } + } else { + (38, 10) // Default for Decimal(None) + }; + + // Allow arrow_precision/arrow_scale to override + let final_precision = arrow_attrs.precision.unwrap_or(precision); + let final_scale = arrow_attrs.scale.unwrap_or(scale); + + if final_precision <= 18 { + quote! { DataType::Decimal64(#final_precision, #final_scale) } + } else if final_precision <= 38 { + quote! { DataType::Decimal128(#final_precision, #final_scale) } + } else { + quote! { DataType::Decimal256(#final_precision, #final_scale) } + } + } else if col_type.starts_with("Money(") { + let precision = arrow_attrs.precision.unwrap_or(19); + let scale = arrow_attrs.scale.unwrap_or(4); + if precision <= 18 { + quote! { DataType::Decimal64(#precision, #scale) } + } else { + quote! { DataType::Decimal128(#precision, #scale) } + } + } else if col_type == "TinyInteger" { + quote! { DataType::Int8 } + } else if col_type == "SmallInteger" { + quote! { DataType::Int16 } + } else if col_type == "Integer" { + quote! { DataType::Int32 } + } else if col_type == "BigInteger" { + quote! { DataType::Int64 } + } else if col_type == "TinyUnsigned" { + quote! { DataType::UInt8 } + } else if col_type == "SmallUnsigned" { + quote! { DataType::UInt16 } + } else if col_type == "Unsigned" { + quote! { DataType::UInt32 } + } else if col_type == "BigUnsigned" { + quote! { DataType::UInt64 } + } else if col_type == "Float" { + quote! { DataType::Float32 } + } else if col_type == "Double" { + quote! { DataType::Float64 } + } else if col_type == "Boolean" { + quote! { DataType::Boolean } + } else if col_type == "Text" { + quote! { DataType::LargeUtf8 } + } else if col_type.starts_with("String(") { + // Parse String(StringLen::N(255)) or String(StringLen::None) + if col_type.contains("None") || col_type.contains("Max") { + quote! { DataType::LargeUtf8 } + } else { + // Try to extract length + if let Some(inner) = col_type + .strip_prefix("String(StringLen::N(") + .and_then(|s| s.strip_suffix("))")) + { + if let Ok(n) = inner.parse::() { + if n <= 32767 { + return quote! { DataType::Utf8 }; + } + } + } + quote! { DataType::LargeUtf8 } + } + } else if col_type.starts_with("Char(") { + quote! { DataType::Utf8 } + } else if col_type == "Date" { + quote! { DataType::Date32 } + } else if col_type == "Time" { + quote! { DataType::Time64(TimeUnit::Microsecond) } + } else if col_type == "DateTime" || col_type == "Timestamp" { + generate_timestamp_datatype(arrow_attrs, false) + } else if col_type == "TimestampWithTimeZone" { + generate_timestamp_datatype(arrow_attrs, true) + } else if col_type.starts_with("Binary(") || col_type.starts_with("VarBinary(") { + if let Some(bw) = arrow_attrs.byte_width { + quote! { DataType::FixedSizeBinary(#bw) } + } else { + quote! { DataType::Binary } + } + } else if col_type == "Json" || col_type == "JsonBinary" { + quote! { DataType::Utf8 } + } else if col_type == "Uuid" { + quote! { DataType::Binary } + } else if col_type.starts_with("Enum {") { + quote! { DataType::Utf8 } + } else { + // Default fallback + quote! { DataType::Binary } + } +} + +/// Map Rust type to Arrow DataType (when no column_type specified) +fn rust_type_to_arrow_datatype(field_type: &Type, arrow_attrs: &ArrowFieldAttrs) -> TokenStream { + let type_string = quote! { #field_type }.to_string().replace(' ', ""); + + // Strip Option<> wrapper if present + let inner_type = if type_string.starts_with("Option<") { + type_string + .strip_prefix("Option<") + .and_then(|s| s.strip_suffix('>')) + .unwrap_or(&type_string) + } else { + &type_string + }; + + match inner_type { + "i8" => quote! { DataType::Int8 }, + "i16" => quote! { DataType::Int16 }, + "i32" => quote! { DataType::Int32 }, + "i64" => quote! { DataType::Int64 }, + "u8" => quote! { DataType::UInt8 }, + "u16" => quote! { DataType::UInt16 }, + "u32" => quote! { DataType::UInt32 }, + "u64" => quote! { DataType::UInt64 }, + "f32" => quote! { DataType::Float32 }, + "f64" => quote! { DataType::Float64 }, + "bool" => quote! { DataType::Boolean }, + "String" => quote! { DataType::Utf8 }, + s if s.contains("Decimal") => { + let precision = arrow_attrs.precision.unwrap_or(38); + let scale = arrow_attrs.scale.unwrap_or(10); + if precision <= 18 { + quote! { DataType::Decimal64(#precision, #scale) } + } else if precision <= 38 { + quote! { DataType::Decimal128(#precision, #scale) } + } else { + quote! { DataType::Decimal256(#precision, #scale) } + } + } + s if (s.contains("DateTime") && s.contains("Offset")) + || (s.contains("DateTime") && s.contains("Utc")) + || (s.contains("DateTime") && s.contains("TimeZone")) + || s.contains("Timestamp") => + { + generate_timestamp_datatype(arrow_attrs, true) + } + s if s.contains("DateTime") => { + generate_timestamp_datatype(arrow_attrs, arrow_attrs.timezone.is_some()) + } + s if s.contains("Date") => quote! { DataType::Date32 }, + s if s.contains("Time") => quote! { DataType::Time64(TimeUnit::Microsecond) }, + "Vec" => { + if let Some(bw) = arrow_attrs.byte_width { + quote! { DataType::FixedSizeBinary(#bw) } + } else { + quote! { DataType::Binary } + } + } + _ => quote! { DataType::Binary }, // Safe fallback + } +} + +/// Generate timestamp DataType with optional timezone +fn generate_timestamp_datatype(arrow_attrs: &ArrowFieldAttrs, has_timezone: bool) -> TokenStream { + let unit = match arrow_attrs.timestamp_unit.as_deref() { + Some("Second") => quote! { TimeUnit::Second }, + Some("Millisecond") => quote! { TimeUnit::Millisecond }, + Some("Microsecond") => quote! { TimeUnit::Microsecond }, + Some("Nanosecond") => quote! { TimeUnit::Nanosecond }, + _ => quote! { TimeUnit::Microsecond }, // Default + }; + + if has_timezone { + let tz = arrow_attrs.timezone.as_deref().unwrap_or("UTC"); + quote! { DataType::Timestamp(#unit, Some(#tz.into())) } + } else if let Some(tz) = &arrow_attrs.timezone { + quote! { DataType::Timestamp(#unit, Some(#tz.into())) } + } else { + quote! { DataType::Timestamp(#unit, None) } + } +} diff --git a/sea-orm-macros/src/derives/attributes.rs b/sea-orm-macros/src/derives/attributes.rs index 0b51945b01..a5cab499af 100644 --- a/sea-orm-macros/src/derives/attributes.rs +++ b/sea-orm-macros/src/derives/attributes.rs @@ -3,41 +3,88 @@ pub mod derive_attr { /// Attributes for Models and ActiveModels #[derive(Default, FromAttributes)] + #[allow(dead_code)] pub struct SeaOrm { pub column: Option, pub entity: Option, pub model: Option, + pub model_ex: Option, + pub active_model: Option, + pub active_model_ex: Option, pub primary_key: Option, pub relation: Option, - pub schema_name: Option, - pub table_name: Option, + pub schema_name: Option, + pub table_name: Option, + pub comment: Option, pub table_iden: Option<()>, + pub rename_all: Option, } } -pub mod field_attr { +pub mod relation_attr { use bae::FromAttributes; - /// Operations for Models and ActiveModels + /// Attributes for Relation enum #[derive(Default, FromAttributes)] pub struct SeaOrm { pub belongs_to: Option, pub has_one: Option, pub has_many: Option, + pub via_rel: Option, pub on_update: Option, pub on_delete: Option, pub on_condition: Option, pub from: Option, pub to: Option, pub fk_name: Option, + pub skip_fk: Option<()>, pub condition_type: Option, } } +pub mod compound_attr { + use bae::FromAttributes; + + /// Attributes for compound model fields + #[derive(Default, FromAttributes)] + pub struct SeaOrm { + pub has_one: Option<()>, + pub has_many: Option<()>, + pub belongs_to: Option<()>, + pub self_ref: Option<()>, + pub skip_fk: Option<()>, + pub via: Option, + pub via_rel: Option, + pub from: Option, + pub to: Option, + pub relation_enum: Option, + pub relation_reverse: Option, + pub reverse: Option<()>, + pub on_update: Option, + pub on_delete: Option, + } +} + +pub mod value_type_attr { + use bae::FromAttributes; + + /// Attributes for compound model fields + #[derive(Default, FromAttributes)] + pub struct SeaOrm { + pub column_type: Option, + pub array_type: Option, + pub value_type: Option, + pub from_str: Option, + pub to_str: Option, + pub try_from_u64: Option<()>, + } +} + +#[cfg(feature = "seaography")] pub mod related_attr { use bae::FromAttributes; - /// Operations for RelatedEntity enumeration + /// Attributes for RelatedEntity enum #[derive(Default, FromAttributes)] pub struct SeaOrm { /// diff --git a/sea-orm-macros/src/derives/case_style.rs b/sea-orm-macros/src/derives/case_style.rs new file mode 100644 index 0000000000..917dda05af --- /dev/null +++ b/sea-orm-macros/src/derives/case_style.rs @@ -0,0 +1,133 @@ +//! Copied from https://github.com/Peternator7/strum/blob/master/strum_macros/src/helpers/case_style.rs +use heck::{ + ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, +}; +use std::str::FromStr; +use syn::{ + Ident, LitStr, + meta::ParseNestedMeta, + parse::{Parse, ParseStream}, +}; + +#[allow(clippy::enum_variant_names)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CaseStyle { + CamelCase, + KebabCase, + MixedCase, + ShoutySnakeCase, + SnakeCase, + TitleCase, + UpperCase, + LowerCase, + ScreamingKebabCase, + PascalCase, +} + +const VALID_CASE_STYLES: &[&str] = &[ + "camelCase", + "PascalCase", + "kebab-case", + "snake_case", + "SCREAMING_SNAKE_CASE", + "SCREAMING-KEBAB-CASE", + "lowercase", + "UPPERCASE", + "title_case", + "mixed_case", +]; + +impl Parse for CaseStyle { + fn parse(input: ParseStream) -> syn::Result { + let text = input.parse::()?; + let val = text.value(); + + val.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected case style for serialize_all: `{val}`. Valid values are: `{VALID_CASE_STYLES:?}`", + ), + ) + }) + } +} + +impl FromStr for CaseStyle { + type Err = (); + + fn from_str(text: &str) -> Result { + Ok(match text { + "camel_case" | "PascalCase" => CaseStyle::PascalCase, + "camelCase" => CaseStyle::CamelCase, + "snake_case" | "snek_case" => CaseStyle::SnakeCase, + "kebab_case" | "kebab-case" => CaseStyle::KebabCase, + "SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase, + "shouty_snake_case" | "shouty_snek_case" | "SCREAMING_SNAKE_CASE" => { + CaseStyle::ShoutySnakeCase + } + "title_case" => CaseStyle::TitleCase, + "mixed_case" => CaseStyle::MixedCase, + "lowercase" => CaseStyle::LowerCase, + "UPPERCASE" => CaseStyle::UpperCase, + _ => return Err(()), + }) + } +} + +pub trait CaseStyleHelpers { + fn convert_case(&self, case_style: Option) -> String; +} + +impl CaseStyleHelpers for Ident { + fn convert_case(&self, case_style: Option) -> String { + let ident_string = self.to_string(); + if let Some(case_style) = case_style { + match case_style { + CaseStyle::PascalCase => ident_string.to_upper_camel_case(), + CaseStyle::KebabCase => ident_string.to_kebab_case(), + CaseStyle::MixedCase => ident_string.to_lower_camel_case(), + CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(), + CaseStyle::SnakeCase => ident_string.to_snake_case(), + CaseStyle::TitleCase => ident_string.to_title_case(), + CaseStyle::UpperCase => ident_string.to_uppercase(), + CaseStyle::LowerCase => ident_string.to_lowercase(), + CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(), + CaseStyle::CamelCase => { + let camel_case = ident_string.to_upper_camel_case(); + let mut pascal = String::with_capacity(camel_case.len()); + let mut it = camel_case.chars(); + if let Some(ch) = it.next() { + pascal.extend(ch.to_lowercase()); + } + pascal.extend(it); + pascal + } + } + } else { + ident_string + } + } +} + +impl TryFrom<&ParseNestedMeta<'_>> for CaseStyle { + type Error = syn::Error; + + fn try_from(value: &ParseNestedMeta) -> Result { + let meta_string_literal: LitStr = value.value()?.parse()?; + let value_string = meta_string_literal.value(); + match CaseStyle::from_str(value_string.as_str()) { + Ok(rule) => Ok(rule), + Err(()) => Err(value.error(format!( + "Unknown value for attribute parameter: `{value_string}`. Valid values are: `{VALID_CASE_STYLES:?}`" + ))), + } + } +} + +#[test] +fn test_convert_case() { + let id = Ident::new("test_me", proc_macro2::Span::call_site()); + assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase))); + assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase))); +} diff --git a/sea-orm-macros/src/derives/column.rs b/sea-orm-macros/src/derives/column.rs index dbc9017ada..b61b73ef8e 100644 --- a/sea-orm-macros/src/derives/column.rs +++ b/sea-orm-macros/src/derives/column.rs @@ -1,16 +1,17 @@ +use super::is_static_iden; use heck::{ToLowerCamelCase, ToSnakeCase}; use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; -use syn::{punctuated::Punctuated, token::Comma, Data, DataEnum, Fields, Lit, Meta, Variant}; +use syn::{Data, DataEnum, Expr, Fields, LitStr, Variant}; /// Derive a Column name for an enum type -pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result { +pub fn impl_iden(ident: &Ident, data: &Data) -> syn::Result { let variants = match data { syn::Data::Enum(DataEnum { variants, .. }) => variants, _ => { return Ok(quote_spanned! { ident.span() => compile_error!("you can only derive DeriveColumn on enums"); - }) + }); } }; @@ -23,51 +24,60 @@ pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result = variants .iter() .map(|v| { let mut column_name = v.ident.to_string().to_snake_case(); for attr in v.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { + if !attr.path().is_ident("sea_orm") { continue; } - if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) - { - for meta in list.iter() { - if let Meta::NameValue(nv) = meta { - if let Some(name) = nv.path.get_ident() { - if name == "column_name" { - if let Lit::Str(litstr) = &nv.lit { - column_name = litstr.value(); - } - } - if name == "table_name" { - if let Lit::Str(litstr) = &nv.lit { - column_name = litstr.value(); - } - } - } - } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("column_name") { + column_name = meta.value()?.parse::()?.value(); + } else { + // Reads the value expression to advance the parse stream. + // Some parameters, such as `primary_key`, do not have any value, + // so ignoring an error occurred here. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); } - } + Ok(()) + })?; } - quote! { #column_name } + all_static &= is_static_iden(&column_name); + Ok::(quote! { #column_name }) }) - .collect(); + .collect::>()?; + + let quoted = if all_static { + quote! { + fn quoted(&self) -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(sea_orm::IdenStatic::as_str(self)) + } + } + } else { + quote! {} + }; Ok(quote!( #[automatically_derived] - impl #ident { - fn default_as_str(&self) -> &str { + impl sea_orm::IdenStatic for #ident { + fn as_str(&self) -> &'static str { match self { #(Self::#variant => #name),* } } } + + #[automatically_derived] + impl sea_orm::Iden for #ident { + #quoted + + fn unquoted(&self) -> &str { + sea_orm::IdenStatic::as_str(self) + } + } )) } @@ -78,18 +88,40 @@ pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result _ => { return Ok(quote_spanned! { ident.span() => compile_error!("you can only derive DeriveColumn on enums"); - }) + }); } }; - let columns = data_enum.variants.iter().map(|column| { - let column_iden = column.ident.clone(); - let column_str_snake = column_iden.to_string().to_snake_case(); - let column_str_mixed = column_iden.to_string().to_lower_camel_case(); - quote!( - #column_str_snake | #column_str_mixed => Ok(#ident::#column_iden) - ) - }); + let columns = data_enum + .variants + .iter() + .map(|column| { + let column_iden = column.ident.clone(); + let column_str_snake = column_iden.to_string().to_snake_case(); + let column_str_mixed = column_iden.to_string().to_lower_camel_case(); + + let mut column_name = column_str_snake.clone(); + for attr in column.attrs.iter() { + if !attr.path().is_ident("sea_orm") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("column_name") { + column_name = meta.value()?.parse::()?.value(); + } else { + // Reads the value expression to advance the parse stream. + // Some parameters, such as `primary_key`, do not have any value, + // so ignoring an error occurred here. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + Ok(()) + })?; + } + Ok::(quote!( + #column_str_snake | #column_str_mixed | #column_name => Ok(#ident::#column_iden) + )) + }) + .collect::, _>>()?; Ok(quote!( #[automatically_derived] @@ -107,35 +139,12 @@ pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result } pub fn expand_derive_column(ident: &Ident, data: &Data) -> syn::Result { - let impl_iden = expand_derive_custom_column(ident, data)?; - - Ok(quote!( - #impl_iden - - #[automatically_derived] - impl sea_orm::IdenStatic for #ident { - fn as_str(&self) -> &str { - self.default_as_str() - } - } - )) -} - -/// Derive a column with a non_snake_case name -pub fn expand_derive_custom_column(ident: &Ident, data: &Data) -> syn::Result { - let impl_default_as_str = impl_default_as_str(ident, data)?; let impl_col_from_str = impl_col_from_str(ident, data)?; + let impl_iden = impl_iden(ident, data)?; Ok(quote!( - #impl_default_as_str - #impl_col_from_str - #[automatically_derived] - impl sea_orm::Iden for #ident { - fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", sea_orm::IdenStatic::as_str(self)).unwrap(); - } - } + #impl_iden )) } diff --git a/sea-orm-macros/src/derives/derive_iden.rs b/sea-orm-macros/src/derives/derive_iden.rs new file mode 100644 index 0000000000..1cfaed2e0b --- /dev/null +++ b/sea-orm-macros/src/derives/derive_iden.rs @@ -0,0 +1,148 @@ +use heck::ToSnakeCase; +use proc_macro2::{self, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::{ + DataEnum, DataStruct, DeriveInput, Expr, Fields, LitStr, Variant, punctuated::Punctuated, +}; + +pub(super) fn is_static_iden(name: &str) -> bool { + // can only begin with [a-z_] + name.chars() + .take(1) + .all(|c| c == '_' || c.is_ascii_alphabetic()) + && name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) +} + +pub(super) fn impl_iden_for_unit_struct( + ident: &syn::Ident, + iden_str: &str, +) -> proc_macro2::TokenStream { + let quoted = if is_static_iden(iden_str) { + quote! { + fn quoted(&self) -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(#iden_str) + } + } + } else { + quote! {} + }; + quote! { + #[automatically_derived] + impl sea_orm::Iden for #ident { + #quoted + + fn unquoted(&self) -> &str { + #iden_str + } + } + } +} + +fn impl_iden_for_enum( + ident: &syn::Ident, + variants: Punctuated, +) -> proc_macro2::TokenStream { + let variants = variants.iter(); + let mut all_static = true; + + let match_pair: Vec = variants + .map(|v| { + let var_ident = &v.ident; + let var_name = if var_ident == "Table" { + ident + } else { + var_ident + }; + let mut var_name = var_name.to_string().to_snake_case(); + v.attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("iden") { + let litstr: LitStr = meta.value()?.parse()?; + var_name = litstr.value(); + } else { + // Reads the value expression to advance the parse stream. + // Some parameters do not have any value, + // so ignoring an error occurred here. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + Ok(()) + }) + }) + .expect("something something"); + all_static &= is_static_iden(&var_name); + quote! { Self::#var_ident => #var_name } + }) + .collect(); + + let match_arms: TokenStream = quote! { #(#match_pair),* }; + + let quoted = if all_static { + quote! { + fn quoted(&self) -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(match self { + #match_arms + }) + } + } + } else { + quote! {} + }; + + quote! { + #[automatically_derived] + impl sea_orm::Iden for #ident { + #quoted + + fn unquoted(&self) -> &str { + match self { + #match_arms + } + } + } + } +} + +pub fn expand_derive_iden(input: DeriveInput) -> syn::Result { + let DeriveInput { ident, data, .. } = input; + + let mut new_iden: String = ident.to_string().to_snake_case(); + input + .attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("iden") { + let litstr: LitStr = meta.value()?.parse()?; + new_iden = litstr.value(); + } else { + // Reads the value expression to advance the parse stream. + // Some parameters do not have any value, + // so ignoring an error occurred here. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + Ok(()) + }) + })?; + + // Currently we only support enums and unit structs + match data { + syn::Data::Enum(DataEnum { variants, .. }) => { + if variants.is_empty() { + Ok(TokenStream::new()) + } else { + Ok(impl_iden_for_enum(&ident, variants)) + } + } + syn::Data::Struct(DataStruct { + fields: Fields::Unit, + .. + }) => Ok(impl_iden_for_unit_struct(&ident, &new_iden)), + _ => Ok(quote_spanned! { + ident.span() => compile_error!("you can only derive DeriveIden on unit struct or enum"); + }), + } +} diff --git a/sea-orm-macros/src/derives/entity.rs b/sea-orm-macros/src/derives/entity.rs index 5f9a0733c5..bf896bf81e 100644 --- a/sea-orm-macros/src/derives/entity.rs +++ b/sea-orm-macros/src/derives/entity.rs @@ -3,16 +3,19 @@ use std::iter::FromIterator; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use super::attributes::derive_attr; +use super::{attributes::derive_attr, impl_iden_for_unit_struct}; struct DeriveEntity { column_ident: syn::Ident, ident: syn::Ident, model_ident: syn::Ident, + model_ex_ident: syn::Ident, + active_model_ident: syn::Ident, + active_model_ex_ident: syn::Ident, primary_key_ident: syn::Ident, relation_ident: syn::Ident, - schema_name: Option, - table_name: Option, + schema_name: Option, + table_name: Option, } impl DeriveEntity { @@ -22,6 +25,13 @@ impl DeriveEntity { let ident = input.ident; let column_ident = sea_attr.column.unwrap_or_else(|| format_ident!("Column")); let model_ident = sea_attr.model.unwrap_or_else(|| format_ident!("Model")); + let model_ex_ident = sea_attr.model_ex.unwrap_or_else(|| format_ident!("Model")); + let active_model_ident = sea_attr + .active_model + .unwrap_or_else(|| format_ident!("ActiveModel")); + let active_model_ex_ident = sea_attr + .active_model_ex + .unwrap_or_else(|| format_ident!("ActiveModel")); let primary_key_ident = sea_attr .primary_key .unwrap_or_else(|| format_ident!("PrimaryKey")); @@ -36,6 +46,9 @@ impl DeriveEntity { column_ident, ident, model_ident, + model_ex_ident, + active_model_ident, + active_model_ex_ident, primary_key_ident, relation_ident, schema_name, @@ -48,12 +61,14 @@ impl DeriveEntity { let expanded_impl_entity_trait = self.impl_entity_trait(); let expanded_impl_iden = self.impl_iden(); let expanded_impl_iden_static = self.impl_iden_static(); + let expanded_impl_entity_registry = self.impl_entity_registry(); TokenStream::from_iter([ expanded_impl_entity_name, expanded_impl_entity_trait, expanded_impl_iden, expanded_impl_iden_static, + expanded_impl_entity_registry, ]) } @@ -76,7 +91,7 @@ impl DeriveEntity { #expanded_schema_name } - fn table_name(&self) -> &str { + fn table_name(&self) -> &'static str { #table_name } } @@ -87,6 +102,9 @@ impl DeriveEntity { let Self { ident, model_ident, + model_ex_ident, + active_model_ident, + active_model_ex_ident, column_ident, primary_key_ident, relation_ident, @@ -98,6 +116,12 @@ impl DeriveEntity { impl sea_orm::entity::EntityTrait for #ident { type Model = #model_ident; + type ModelEx = #model_ex_ident; + + type ActiveModel = #active_model_ident; + + type ActiveModelEx = #active_model_ex_ident; + type Column = #column_ident; type PrimaryKey = #primary_key_ident; @@ -110,14 +134,17 @@ impl DeriveEntity { fn impl_iden(&self) -> TokenStream { let ident = &self.ident; - quote!( - #[automatically_derived] - impl sea_orm::Iden for #ident { - fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", sea_orm::IdenStatic::as_str(self)).unwrap(); + match &self.table_name { + Some(table_name) => impl_iden_for_unit_struct(ident, &table_name.value()), + None => quote!( + #[automatically_derived] + impl sea_orm::Iden for #ident { + fn unquoted(&self) -> &str { + ::as_str(self) + } } - } - ) + ), + } } fn impl_iden_static(&self) -> TokenStream { @@ -126,12 +153,27 @@ impl DeriveEntity { quote!( #[automatically_derived] impl sea_orm::IdenStatic for #ident { - fn as_str(&self) -> &str { + fn as_str(&self) -> &'static str { ::table_name(self) } } ) } + + fn impl_entity_registry(&self) -> TokenStream { + if cfg!(feature = "entity-registry") { + quote! { + sea_orm::register_entity! { + sea_orm::EntityRegistry { + module_path: module_path!(), + schema_info: |schema| sea_orm::EntitySchemaInfo::new(Entity, schema), + } + } + } + } else { + quote!() + } + } } pub fn expand_derive_entity(input: syn::DeriveInput) -> syn::Result { diff --git a/sea-orm-macros/src/derives/entity_loader.rs b/sea-orm-macros/src/derives/entity_loader.rs new file mode 100644 index 0000000000..7d81cbb35e --- /dev/null +++ b/sea-orm-macros/src/derives/entity_loader.rs @@ -0,0 +1,580 @@ +use proc_macro2::TokenStream; +use quote::quote; +use std::collections::HashMap; +use syn::{Ident, punctuated::Punctuated, token::Comma}; + +#[derive(Default)] +pub struct EntityLoaderSchema { + pub fields: Vec, +} + +pub struct EntityLoaderField { + pub is_one: bool, + pub is_self: bool, + pub is_reverse: bool, + pub field: Ident, + /// super::bakery::Entity + pub entity: String, + pub relation_enum: Option, + pub via: Option, +} + +pub fn expand_entity_loader(schema: EntityLoaderSchema) -> TokenStream { + let mut field_bools: Punctuated<_, Comma> = Punctuated::new(); + let mut field_nests: Punctuated<_, Comma> = Punctuated::new(); + let mut one_fields: Punctuated<_, Comma> = Punctuated::new(); + let mut with_impl = TokenStream::new(); + let mut with_nest_impl = TokenStream::new(); + let mut select_impl = TokenStream::new(); + let mut assemble_one = TokenStream::new(); + let mut load_one = TokenStream::new(); + let mut load_many = TokenStream::new(); + let mut load_one_nest = TokenStream::new(); + let mut load_many_nest = TokenStream::new(); + let mut load_one_nest_nest = TokenStream::new(); + let mut load_many_nest_nest = TokenStream::new(); + let mut into_with_param_impl = TokenStream::new(); + let mut arity = 1; + + let (async_, await_) = if cfg!(feature = "async") { + (quote!(async), quote!(.await)) + } else { + (quote!(), quote!()) + }; + + let async_trait = if cfg!(feature = "async") { + quote!(#[async_trait::async_trait]) + } else { + quote!() + }; + + one_fields.push(quote!(model)); + + let mut total_count = HashMap::new(); + for entity_field in schema.fields.iter() { + *total_count.entry(&entity_field.entity).or_insert(0) += 1; + } + + for entity_field in schema.fields.iter() { + let field = &entity_field.field; + let is_one = entity_field.is_one; + let is_self = entity_field.is_self; + let is_reverse = entity_field.is_reverse; + let entity: TokenStream = entity_field.entity.parse().unwrap(); + let entity_module: TokenStream = entity_field + .entity + .trim_end_matches("::Entity") + .parse() + .unwrap(); + + if !is_self && *total_count.get(&entity_field.entity).unwrap() != 1 { + // prevent impl trait for same entity twice + // self_ref is allowed + continue; + } + + if !is_self { + field_bools.push(quote! { + #[doc = " Generated by sea-orm-macros"] + pub #field: bool + }); + field_nests.push(quote! { + #[doc = " Generated by sea-orm-macros"] + pub #field: #entity_module::EntityLoaderWith + }); + + with_impl.extend(quote! { + if target == sea_orm::compound::LoadTarget::TableRef(#entity.table_ref()) { + self.#field = true; + } + }); + with_nest_impl.extend(quote! { + if left == sea_orm::compound::LoadTarget::TableRef(#entity.table_ref()) { + self.with.#field = true; + self.nest.#field.set(right); + return self; + } + }); + } else { + field_bools.push(quote! { + #[doc = " Generated by sea-orm-macros"] + pub #field: bool + }); + field_nests.push(quote! { + #[doc = " Generated by sea-orm-macros"] + pub #field: EntityLoaderWith + }); + + if let Some(relation_enum) = &entity_field.relation_enum { + with_impl.extend(quote! { + if let sea_orm::compound::LoadTarget::Relation(relation_enum) = &target { + if relation_enum == #relation_enum { + self.#field = true; + } + } + }); + } + + if let Some(via_lit) = &entity_field.via { + let via = Ident::new(&via_lit.value(), via_lit.span()); + let target_type = if !is_reverse { + Ident::new("TableRef", via_lit.span()) + } else { + Ident::new("TableRefRev", via_lit.span()) + }; + + let target_entity = if !is_reverse { + quote!(super::#via::Entity) + } else { + quote!(super::#via::EntityReverse) + }; + + with_impl.extend(quote! { + if target == sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()) { + self.#field = true; + } + }); + with_nest_impl.extend(quote! { + if left == sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()) { + self.with.#field = true; + self.nest.#field.set(right); + return self; + } + }); + + into_with_param_impl.extend(quote! { + impl EntityLoaderWithParam for #target_entity { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + (sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()), None) + } + } + + impl EntityLoaderWithParam for (#target_entity, S) + where + S: EntityTrait, + Entity: Related, + { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + ( + sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()), + Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())), + ) + } + } + }); + } + } + + if is_one && !is_self { + arity += 1; + if arity <= 3 { + // do not go beyond SelectThree + one_fields.push(quote!(#field)); + + select_impl.extend(quote! { + let select = if self.with.#field && self.nest.#field.is_empty() { + self.with.#field = false; + loaded.#field = true; + select.find_also(Entity, #entity) + } else { + select.select_also_fake(#entity) + }; + }); + + assemble_one.extend(quote! { + if loaded.#field { + model.#field = #field.map(Into::into).map(Box::new).into(); + } + }); + } + + load_one.extend(quote! { + if with.#field { + let #field = models.as_slice().load_one_ex(#entity, db)#await_?; + let #field = #entity_module::EntityLoader::load_nest(#field, &nest.#field, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.map(Into::into).map(Box::new).into(); + } + } + }); + load_one_nest.extend(quote! { + if with.#field { + let #field = models.as_slice().load_one_ex(#entity, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + if let Some(model) = model.as_mut() { + model.#field = #field.map(Into::into).map(Box::new).into(); + } + } + } + }); + load_one_nest_nest.extend(quote! { + if with.#field { + let #field = models.as_slice().load_one_ex(#entity, db)#await_?; + + for (models, #field) in models.iter_mut().zip(#field) { + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.map(Into::into).map(Box::new).into(); + } + } + } + }); + } else if !is_one && !is_self { + load_many.extend(quote! { + if with.#field { + let #field = models.as_slice().load_many_ex(#entity, db)#await_?; + let #field = #entity_module::EntityLoader::load_nest_nest(#field, &nest.#field, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.into(); + } + } + }); + load_many_nest.extend(quote! { + if with.#field { + let #field = models.as_slice().load_many_ex(#entity, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + if let Some(model) = model.as_mut() { + model.#field = #field.into(); + } + } + } + }); + load_many_nest_nest.extend(quote! { + if with.#field { + let #field = models.as_slice().load_many_ex(#entity, db)#await_?; + + for (models, #field) in models.iter_mut().zip(#field) { + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.into(); + } + } + } + }); + } else if is_one && is_self { + if let Some(relation_enum) = &entity_field.relation_enum { + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + load_one.extend(quote! { + if with.#field { + let #field = models.as_slice().load_self_ex(#entity, Relation::#relation_enum, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.map(Into::into).map(Box::new).into(); + } + } + }); + } + } else if !is_one && is_self { + if let Some(relation_enum) = &entity_field.relation_enum { + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + load_many.extend(quote! { + if with.#field { + let #field = models.as_slice().load_self_many_ex(#entity, Relation::#relation_enum, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.into(); + } + } + }); + } + if let Some(via) = &entity_field.via { + let via = Ident::new(&via.value(), via.span()); + load_many.extend(quote! { + if with.#field { + let #field = models.as_slice().load_self_via_ex(super::#via::Entity, #is_reverse, db)#await_?; + let #field = EntityLoader::load_nest_nest(#field, &nest.#field, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.into(); + } + } + }); + load_many_nest.extend(quote! { + if with.#field { + let #field = models.as_slice().load_self_via_ex(super::#via::Entity, #is_reverse, db)#await_?; + + for (model, #field) in models.iter_mut().zip(#field) { + if let Some(model) = model.as_mut() { + model.#field = #field.into(); + } + } + } + }); + load_many_nest_nest.extend(quote! { + if with.#field { + let #field = models.as_slice().load_self_via_ex(super::#via::Entity, #is_reverse, db)#await_?; + + for (models, #field) in models.iter_mut().zip(#field) { + for (model, #field) in models.iter_mut().zip(#field) { + model.#field = #field.into(); + } + } + } + }); + } + } + } + + quote! { + + #[doc = " Generated by sea-orm-macros"] + #[derive(Clone)] + pub struct EntityLoader { + select: sea_orm::Select, + with: EntityLoaderWith, + nest: EntityLoaderNest, + } + + #[doc = " Generated by sea-orm-macros"] + #[derive(Debug, Default, Clone, PartialEq, Eq)] + pub struct EntityReverse; + + impl sea_orm::compound::EntityReverse for EntityReverse { + type Entity = Entity; + } + + #[doc = " Generated by sea-orm-macros"] + #[derive(Debug, Default, Clone, PartialEq, Eq)] + pub struct EntityLoaderWith { + #field_bools + } + + #[doc = " Generated by sea-orm-macros"] + #[derive(Debug, Default, Clone, PartialEq, Eq)] + pub struct EntityLoaderNest { + #field_nests + } + + impl Entity { + #[doc = " Generated by sea-orm-macros"] + pub const REVERSE: EntityReverse = EntityReverse; + } + + impl EntityLoaderWith { + #[doc = " Generated by sea-orm-macros"] + pub fn is_empty(&self) -> bool { + self == &Self::default() + } + #[doc = " Generated by sea-orm-macros"] + pub fn set(&mut self, target: sea_orm::compound::LoadTarget) { + #with_impl + } + } + + #[doc = " Parameters for EntityLoader"] + pub trait EntityLoaderWithParam { + #[doc = " Generated by sea-orm-macros"] + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option); + } + + #[automatically_derived] + impl EntityLoaderWithParam for R + where + R: EntityTrait, + Entity: Related, + { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + (sea_orm::compound::LoadTarget::TableRef(self.table_ref()), None) + } + } + + #[automatically_derived] + impl EntityLoaderWithParam for (R, S) + where + R: EntityTrait, + Entity: Related, + S: EntityTrait, + R: Related, + { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + ( + sea_orm::compound::LoadTarget::TableRef(self.0.table_ref()), + Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())), + ) + } + } + + #[automatically_derived] + impl EntityLoaderWithParam for sea_orm::compound::EntityLoaderWithSelf + where + R: EntityTrait, + Entity: Related, + S: EntityTrait, + R: RelatedSelfVia, + { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + ( + sea_orm::compound::LoadTarget::TableRef(self.0.table_ref()), + Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())), + ) + } + } + + #[automatically_derived] + impl EntityLoaderWithParam for sea_orm::compound::EntityLoaderWithSelfRev + where + R: EntityTrait, + Entity: Related, + S: EntityTrait, + R: RelatedSelfVia, + SR: sea_orm::compound::EntityReverse, + { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + ( + sea_orm::compound::LoadTarget::TableRef(self.0.table_ref()), + Some(sea_orm::compound::LoadTarget::TableRefRev(S::default().table_ref())), + ) + } + } + + #[automatically_derived] + impl EntityLoaderWithParam for Relation { + fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { + (sea_orm::compound::LoadTarget::Relation(self.name()), None) + } + } + + #into_with_param_impl + + #[automatically_derived] + impl std::fmt::Debug for EntityLoader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EntityLoader") + .field("select", &match (Entity::default().schema_name(), Entity::default().table_name()) { + (Some(s), t) => format!("{s}.{t}"), + (None, t) => t.to_owned(), + }) + .field("with", &self.with) + .field("nest", &self.nest) + .finish() + } + } + + #[automatically_derived] + impl sea_orm::QueryFilter for EntityLoader { + type QueryStatement = as sea_orm::QueryFilter>::QueryStatement; + + fn query(&mut self) -> &mut Self::QueryStatement { + sea_orm::QueryFilter::query(&mut self.select) + } + } + + #[automatically_derived] + impl sea_orm::QueryOrder for EntityLoader { + type QueryStatement = as sea_orm::QueryOrder>::QueryStatement; + + fn query(&mut self) -> &mut Self::QueryStatement { + sea_orm::QueryOrder::query(&mut self.select) + } + } + + #[automatically_derived] + #async_trait + impl sea_orm::compound::EntityLoaderTrait for EntityLoader { + type ModelEx = ModelEx; + + #async_ fn fetch(self, db: &C, page: u64, page_size: u64) -> Result, sea_orm::DbErr> { + self.fetch(db, page, page_size)#await_ + } + + #async_ fn num_items(self, db: &C, page_size: u64) -> Result { + self.select.paginate(db, page_size).num_items()#await_ + } + } + + impl Entity { + #[doc = " Generated by sea-orm-macros"] + pub fn load() -> EntityLoader { + EntityLoader { + select: Entity::find(), + with: Default::default(), + nest: Default::default(), + } + } + } + + impl EntityLoader { + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn one(mut self, db: &C) -> Result, sea_orm::DbErr> { + use sea_orm::QuerySelect; + + self.select = self.select.limit(1); + Ok(self.all(db)#await_?.into_iter().next()) + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn all(self, db: &C) -> Result, sea_orm::DbErr> { + self.fetch(db, 0, 0)#await_ + } + + #[doc = " Generated by sea-orm-macros"] + pub fn with(mut self, param: T) -> Self { + match param.into_with_param() { + (left, None) => self.with_1(left), + (left, Some(right)) => self.with_2(left, right), + } + } + + fn with_1(mut self, load_target: sea_orm::compound::LoadTarget) -> Self { + self.with.set(load_target); + self + } + + fn with_2(mut self, left: sea_orm::compound::LoadTarget, right: sea_orm::compound::LoadTarget) -> Self { + #with_nest_impl + self + } + + #[doc = " Generated by sea-orm-macros"] + #async_ fn fetch(mut self, db: &C, page: u64, page_size: u64) -> Result, sea_orm::DbErr> { + let select = self.select; + let mut loaded = EntityLoaderWith::default(); + + #select_impl + + let models = if page_size != 0 { + select.paginate(db, page_size).fetch_page(page)#await_? + } else { + select.all(db)#await_? + }; + + let models = models.into_iter().map(|(#one_fields)| { + let mut model = model.into_ex(); + #assemble_one + model + }).collect::>(); + + let models = Self::load(models, &self.with, &self.nest, db)#await_?; + + Ok(models) + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn load(mut models: Vec, with: &EntityLoaderWith, nest: &EntityLoaderNest, db: &C) -> Result, DbErr> { + use sea_orm::LoaderTraitEx; + #load_one + #load_many + Ok(models) + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn load_nest(mut models: Vec>, with: &EntityLoaderWith, db: &C) -> Result>, DbErr> { + use sea_orm::LoaderTraitEx; + #load_one_nest + #load_many_nest + Ok(models) + } + + #[doc = " Generated by sea-orm-macros"] + pub #async_ fn load_nest_nest(mut models: Vec>, with: &EntityLoaderWith, db: &C) -> Result>, DbErr> { + use sea_orm::NestedLoaderTrait; + #load_one_nest_nest + #load_many_nest_nest + Ok(models) + } + } + + } +} diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index e50b8a66d4..63a4680b1e 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -1,50 +1,143 @@ +use super::case_style::{CaseStyle, CaseStyleHelpers}; use super::util::{escape_rust_keyword, trim_starting_raw_identifier}; -use heck::{ToSnakeCase, ToUpperCamelCase}; +use heck::{ + ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, +}; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::quote; +use std::str::FromStr; +use syn::meta::ParseNestedMeta; use syn::{ - parse::Error, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Fields, - Lit, LitStr, Meta, Type, + Attribute, Data, Fields, Lit, LitStr, punctuated::Punctuated, spanned::Spanned, token::Comma, }; +const NOT_AUTO_INCRE_TYPE_SUFFIX: [&str; 2] = ["String", "Uuid"]; + +#[allow(dead_code)] +fn convert_case(s: &str, case_style: CaseStyle) -> String { + match case_style { + CaseStyle::PascalCase => s.to_upper_camel_case(), + CaseStyle::KebabCase => s.to_kebab_case(), + CaseStyle::MixedCase => s.to_lower_camel_case(), + CaseStyle::ShoutySnakeCase => s.to_shouty_snake_case(), + CaseStyle::SnakeCase => s.to_snake_case(), + CaseStyle::TitleCase => s.to_title_case(), + CaseStyle::UpperCase => s.to_uppercase(), + CaseStyle::LowerCase => s.to_lowercase(), + CaseStyle::ScreamingKebabCase => s.to_kebab_case().to_uppercase(), + CaseStyle::CamelCase => { + let camel_case = s.to_upper_camel_case(); + let mut result = String::with_capacity(camel_case.len()); + let mut it = camel_case.chars(); + if let Some(ch) = it.next() { + result.extend(ch.to_lowercase()); + } + result.extend(it); + result + } + } +} + +#[cfg(feature = "with-json")] +fn serde_deserialize_name( + orig: &str, + serde_rename: Option<&str>, + serde_rename_all: Option, +) -> String { + if let Some(rename) = serde_rename.as_ref() { + return rename.to_string(); + } + + if let Some(case_style) = serde_rename_all { + convert_case(orig, case_style) + } else { + orig.to_string() + } +} + +fn consume_meta(meta: ParseNestedMeta<'_>) { + let _ = meta.value().and_then(|v| v.parse::()); +} + /// Method to derive an Model -pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Result { +pub fn expand_derive_entity_model(data: &Data, attrs: &[Attribute]) -> syn::Result { // if #[sea_orm(table_name = "foo", schema_name = "bar")] specified, create Entity struct let mut table_name = None; + let mut comment = quote! {None}; let mut schema_name = quote! { None }; let mut table_iden = false; - attrs.iter().for_each(|attr| { - if attr.path.get_ident().map(|i| i == "sea_orm") != Some(true) { - return; - } + let mut model_ex = false; + let mut rename_all: Option = None; + let mut serde_rename_all: Option = None; - if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { - for meta in list.iter() { - if let Meta::NameValue(nv) = meta { - if let Some(ident) = nv.path.get_ident() { - if ident == "table_name" { - table_name = Some(nv.lit.clone()); - } else if ident == "schema_name" { - let name = &nv.lit; - schema_name = quote! { Some(#name) }; - } - } - } else if let Meta::Path(path) = meta { - if let Some(ident) = path.get_ident() { - if ident == "table_iden" { - table_iden = true; - } + // Parse #[serde(rename_all = "...")] at struct level + attrs + .iter() + .filter(|attr| attr.path().is_ident("serde")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename_all") { + if let Ok(lit) = meta.value().and_then(|v| v.parse::()) { + // #[serde(rename_all = "camelCase")] + serde_rename_all = CaseStyle::from_str(&lit.value()).ok(); + } else { + // #[serde(rename_all(serialize = "...", deserialize = "..."))] + meta.parse_nested_meta(|nested| { + if nested.path.is_ident("deserialize") { + let lit: LitStr = nested.value()?.parse()?; + serde_rename_all = CaseStyle::from_str(&lit.value()).ok(); + } else { + consume_meta(nested); + } + Ok(()) + })?; } + } else { + consume_meta(meta); } - } - } - }); + Ok(()) + }) + })?; + + attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("comment") { + let name: Lit = meta.value()?.parse()?; + comment = quote! { Some(#name) }; + } else if meta.path.is_ident("table_name") { + table_name = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("schema_name") { + let name: Lit = meta.value()?.parse()?; + schema_name = quote! { Some(#name) }; + } else if meta.path.is_ident("table_iden") { + table_iden = true; + } else if meta.path.is_ident("model_ex") { + model_ex = true; + } else if meta.path.is_ident("rename_all") { + rename_all = Some((&meta).try_into()?); + } else { + consume_meta(meta); + } + + Ok(()) + }) + })?; + let entity_def = table_name .as_ref() .map(|table_name| { + let entity_extra_attr = if model_ex { + quote!(#[sea_orm(model_ex = ModelEx, active_model_ex = ActiveModelEx)]) + } else { + quote!() + }; quote! { #[doc = " Generated by sea-orm-macros"] #[derive(Copy, Clone, Default, Debug, sea_orm::prelude::DeriveEntity)] + #entity_extra_attr pub struct Entity; #[automatically_derived] @@ -53,9 +146,13 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res #schema_name } - fn table_name(&self) -> &str { + fn table_name(&self) -> &'static str { #table_name } + + fn comment(&self) -> Option<&str> { + #comment + } } } }) @@ -64,13 +161,17 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res // generate Column enum and it's ColumnTrait impl let mut columns_enum: Punctuated<_, Comma> = Punctuated::new(); let mut columns_trait: Punctuated<_, Comma> = Punctuated::new(); + let mut columns_enum_type_name: Punctuated<_, Comma> = Punctuated::new(); let mut columns_select_as: Punctuated<_, Comma> = Punctuated::new(); let mut columns_save_as: Punctuated<_, Comma> = Punctuated::new(); let mut primary_keys: Punctuated<_, Comma> = Punctuated::new(); let mut primary_key_types: Punctuated<_, Comma> = Punctuated::new(); - let mut auto_increment = true; + let mut auto_increment: Option = None; + #[cfg(feature = "with-json")] + let mut columns_json_keys: Punctuated<_, Comma> = Punctuated::new(); + if table_iden { - if let Some(table_name) = table_name { + if let Some(table_name) = &table_name { let table_field_name = Ident::new("Table", Span::call_site()); columns_enum.push(quote! { #[doc = " Generated by sea-orm-macros"] @@ -84,25 +185,36 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } } if let Data::Struct(item_struct) = data { - if let Fields::Named(fields) = item_struct.fields { - for field in fields.named { + if let Fields::Named(fields) = &item_struct.fields { + for field in &fields.named { if let Some(ident) = &field.ident { let original_field_name = trim_starting_raw_identifier(ident); - let mut field_name = Ident::new( - &original_field_name.to_upper_camel_case(), - Span::call_site(), - ); + let mut field_name = + Ident::new(&original_field_name.to_upper_camel_case(), ident.span()); let mut nullable = false; let mut default_value = None; + let mut comment = None; let mut default_expr = None; let mut select_as = None; let mut save_as = None; + let mut unique_key = None; + let mut renamed_from = None; let mut indexed = false; let mut ignore = false; let mut unique = false; let mut sql_type = None; - let mut column_name = if original_field_name + let mut enum_name = None; + let mut is_primary_key = false; + let mut is_auto_increment = false; + let mut extra = None; + let mut seaography_ignore = false; + #[cfg(feature = "with-json")] + let mut serde_rename: Option = None; + + let mut column_name = if let Some(case_style) = rename_all { + Some(field_name.convert_case(Some(case_style))) + } else if original_field_name != original_field_name.to_upper_camel_case().to_snake_case() { // `to_snake_case` was used to trim prefix and tailing underscore @@ -110,122 +222,172 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } else { None }; - let mut enum_name = None; - let mut is_primary_key = false; - // search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(Some(255))", default_value = "new user", default_expr = "gen_random_uuid()", column_name = "name", enum_name = "Name", nullable, indexed, unique)] - for attr in field.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { - continue; - } - // single param - if let Ok(list) = - attr.parse_args_with(Punctuated::::parse_terminated) - { - for meta in list.iter() { - match meta { - Meta::NameValue(nv) => { - if let Some(name) = nv.path.get_ident() { - if name == "column_type" { - if let Lit::Str(litstr) = &nv.lit { - let ty: TokenStream = - syn::parse_str(&litstr.value())?; - sql_type = Some(ty); - } else { - return Err(Error::new( - field.span(), - format!("Invalid column_type {:?}", nv.lit), - )); - } - } else if name == "auto_increment" { - if let Lit::Bool(litbool) = &nv.lit { - auto_increment = litbool.value(); - } else { - return Err(Error::new( - field.span(), - format!( - "Invalid auto_increment = {:?}", - nv.lit - ), - )); - } - } else if name == "default_value" { - default_value = Some(nv.lit.to_owned()); - } else if name == "default_expr" { - default_expr = Some(nv.lit.to_owned()); - } else if name == "column_name" { - if let Lit::Str(litstr) = &nv.lit { - column_name = Some(litstr.value()); - } else { - return Err(Error::new( - field.span(), - format!("Invalid column_name {:?}", nv.lit), - )); - } - } else if name == "enum_name" { - if let Lit::Str(litstr) = &nv.lit { - let ty: Ident = - syn::parse_str(&litstr.value())?; - enum_name = Some(ty); - } else { - return Err(Error::new( - field.span(), - format!("Invalid enum_name {:?}", nv.lit), - )); - } - } else if name == "select_as" { - if let Lit::Str(litstr) = &nv.lit { - select_as = Some(litstr.value()); - } else { - return Err(Error::new( - field.span(), - format!("Invalid select_as {:?}", nv.lit), - )); - } - } else if name == "save_as" { - if let Lit::Str(litstr) = &nv.lit { - save_as = Some(litstr.value()); - } else { - return Err(Error::new( - field.span(), - format!("Invalid save_as {:?}", nv.lit), - )); - } - } - } + // search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(StringLen::N(255))", default_value = "new user", default_expr = "gen_random_uuid()", column_name = "name", enum_name = "Name", nullable, indexed, unique)] + for attr in field.attrs.iter() { + if attr.path().is_ident("sea_orm") { + // single param + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("column_type") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + let ty: TokenStream = syn::parse_str(&litstr.value())?; + sql_type = Some(ty); + } else { + return Err( + meta.error(format!("Invalid column_type {lit:?}")) + ); } - Meta::Path(p) => { - if let Some(name) = p.get_ident() { - if name == "ignore" { - ignore = true; - break; - } else if name == "primary_key" { - is_primary_key = true; - primary_key_types.push(field.ty.clone()); - } else if name == "nullable" { - nullable = true; - } else if name == "indexed" { - indexed = true; - } else if name == "unique" { - unique = true; + } else if meta.path.is_ident("auto_increment") { + let lit = meta.value()?.parse()?; + if let Lit::Bool(litbool) = lit { + is_auto_increment = litbool.value(); + auto_increment = Some(litbool.value()); + } else { + return Err( + meta.error(format!("Invalid auto_increment = {lit:?}")) + ); + } + } else if meta.path.is_ident("comment") { + comment = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("default_value") { + default_value = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("default_expr") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + let value_expr: TokenStream = + syn::parse_str(&litstr.value())?; + default_expr = Some(value_expr); + } else { + return Err( + meta.error(format!("Invalid column_type {lit:?}")) + ); + } + } else if meta.path.is_ident("column_name") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + column_name = Some(litstr.value()); + } else { + return Err( + meta.error(format!("Invalid column_name {lit:?}")) + ); + } + } else if meta.path.is_ident("enum_name") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + let ty: Ident = syn::parse_str(&litstr.value())?; + enum_name = Some(ty); + } else { + return Err( + meta.error(format!("Invalid enum_name {lit:?}")) + ); + } + } else if meta.path.is_ident("select_as") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + select_as = Some(litstr.value()); + } else { + return Err( + meta.error(format!("Invalid select_as {lit:?}")) + ); + } + } else if meta.path.is_ident("save_as") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + save_as = Some(litstr.value()); + } else { + return Err(meta.error(format!("Invalid save_as {lit:?}"))); + } + } else if meta.path.is_ident("ignore") { + ignore = true; + } else if meta.path.is_ident("primary_key") { + is_primary_key = true; + primary_key_types.push(field.ty.clone()); + } else if meta.path.is_ident("nullable") { + nullable = true; + } else if meta.path.is_ident("indexed") { + indexed = true; + } else if meta.path.is_ident("unique") { + unique = true; + } else if meta.path.is_ident("unique_key") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + unique_key = Some(litstr.value()); + } else { + return Err( + meta.error(format!("Invalid unique_key {lit:?}")) + ); + } + } else if meta.path.is_ident("renamed_from") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + renamed_from = Some(litstr.value()); + } else { + return Err( + meta.error(format!("Invalid renamed_from {lit:?}")) + ); + } + } else if meta.path.is_ident("extra") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + extra = Some(litstr.value()); + } else { + return Err(meta.error(format!("Invalid extra {lit:?}"))); + } + } else { + consume_meta(meta); + } + + Ok(()) + })?; + } else if attr.path().is_ident("seaography") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("ignore") { + seaography_ignore = true; + } + + Ok(()) + })?; + } else if cfg!(feature = "with-json") && attr.path().is_ident("serde") { + #[cfg(feature = "with-json")] + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + if let Ok(lit) = meta.value().and_then(|v| v.parse::()) + { + // #[serde(rename = "xxx")] + serde_rename = Some(lit.value()); + } else { + // #[serde(rename(serialize = "...", deserialize = "..."))] + meta.parse_nested_meta(|nested| { + if nested.path.is_ident("deserialize") { + let lit: LitStr = nested.value()?.parse()?; + serde_rename = Some(lit.value()); + } else { + consume_meta(nested); } - } + Ok(()) + })?; } - _ => {} + } else { + consume_meta(meta); } - } + Ok(()) + })?; } } + #[cfg(feature = "with-json")] + let json_key_name = serde_deserialize_name( + &original_field_name, + serde_rename.as_deref(), + serde_rename_all, + ); + if let Some(enum_name) = enum_name { field_name = enum_name; } - field_name = Ident::new(&escape_rust_keyword(field_name), Span::call_site()); + field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); let variant_attrs = match &column_name { Some(column_name) => quote! { @@ -237,6 +399,11 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res }, }; + let field_type = &field.ty; + let field_type = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + if ignore { continue; } else { @@ -244,6 +411,10 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res #variant_attrs #field_name }); + #[cfg(feature = "with-json")] + columns_json_keys.push(quote! { + Self::#field_name => #json_key_name + }); } if is_primary_key { @@ -253,78 +424,51 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res }); } + if !is_primary_key && is_auto_increment { + return Err(syn::Error::new_spanned( + ident, + "auto_increment can only be used on primary_key", + )); + } + + if primary_key_types.len() > 1 && is_auto_increment { + return Err(syn::Error::new_spanned( + ident, + "auto_increment cannot be used on composite primary_key", + )); + } + if let Some(select_as) = select_as { columns_select_as.push(quote! { - Self::#field_name => expr.cast_as(sea_orm::sea_query::Alias::new(#select_as)) + Self::#field_name => sea_orm::sea_query::ExprTrait::cast_as(expr, #select_as) }); } if let Some(save_as) = save_as { columns_save_as.push(quote! { - Self::#field_name => val.cast_as(sea_orm::sea_query::Alias::new(#save_as)) + Self::#field_name => sea_orm::sea_query::ExprTrait::cast_as(val, #save_as) }); } - let field_type = &field.ty; - let field_type = quote! { #field_type } - .to_string() //E.g.: "Option < String >" - .replace(' ', ""); // Remove spaces let field_type = if field_type.starts_with("Option<") { nullable = true; - &field_type[7..(field_type.len() - 1)] // Extract `T` out of `Option` + &field_type["Option<".len()..(field_type.len() - 1)] // Extract `T` out of `Option` } else { field_type.as_str() }; + let field_span = field.span(); - let sea_query_col_type = match sql_type { - Some(t) => quote! { sea_orm::prelude::ColumnType::#t }, - None => { - let col_type = match field_type { - "char" => quote! { Char(None) }, - "String" | "&str" => quote! { String(None) }, - "i8" => quote! { TinyInteger }, - "u8" => quote! { TinyUnsigned }, - "i16" => quote! { SmallInteger }, - "u16" => quote! { SmallUnsigned }, - "i32" => quote! { Integer }, - "u32" => quote! { Unsigned }, - "i64" => quote! { BigInteger }, - "u64" => quote! { BigUnsigned }, - "f32" => quote! { Float }, - "f64" => quote! { Double }, - "bool" => quote! { Boolean }, - "Date" | "NaiveDate" => quote! { Date }, - "Time" | "NaiveTime" => quote! { Time }, - "DateTime" | "NaiveDateTime" => { - quote! { DateTime } - } - "DateTimeUtc" | "DateTimeLocal" | "DateTimeWithTimeZone" => { - quote! { TimestampWithTimeZone } - } - "Uuid" => quote! { Uuid }, - "Json" => quote! { Json }, - "Decimal" => quote! { Decimal(None) }, - "Vec" => { - quote! { Binary(sea_orm::sea_query::BlobSize::Blob(None)) } - } - _ => { - // Assumed it's ActiveEnum if none of the above type matches - quote! {} - } - }; - if col_type.is_empty() { - let field_span = field.span(); - let ty: Type = LitStr::new(field_type, field_span).parse()?; - let def = quote_spanned! { field_span => - std::convert::Into::::into( - <#ty as sea_orm::sea_query::ValueType>::column_type() - ) - }; - quote! { #def } - } else { - quote! { sea_orm::prelude::ColumnType::#col_type } + if is_primary_key && auto_increment.is_none() { + for suffix in NOT_AUTO_INCRE_TYPE_SUFFIX { + if field_type.ends_with(suffix) { + auto_increment = Some(false); + break; } } - }; + } + + let sea_query_col_type = + super::value_type_match::column_type_expr(sql_type, field_type, field_span); + let col_def = quote! { sea_orm::prelude::ColumnTypeTrait::def(#sea_query_col_type) }; @@ -338,13 +482,39 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res if unique { match_row = quote! { #match_row.unique() }; } + if seaography_ignore { + match_row = quote! { #match_row.seaography_ignore() }; + } + if unique_key.is_some() { + match_row = quote! { #match_row.unique_key(#unique_key) }; + } + if renamed_from.is_some() { + match_row = quote! { #match_row.renamed_from(#renamed_from) }; + } if let Some(default_value) = default_value { match_row = quote! { #match_row.default_value(#default_value) }; } + if let Some(comment) = comment { + match_row = quote! { #match_row.comment(#comment) }; + } if let Some(default_expr) = default_expr { - match_row = quote! { #match_row.default_expr(#default_expr) }; + match_row = quote! { #match_row.default(#default_expr) }; + } + if let Some(extra) = extra { + match_row = quote! { #match_row.extra(#extra) }; } + // match_row = quote! { #match_row.comment() }; columns_trait.push(match_row); + + let ty: syn::Type = syn::LitStr::new(field_type, field_span) + .parse() + .expect("field type error"); + let enum_type_name = quote::quote_spanned! { field_span => + <#ty as sea_orm::sea_query::ValueType>::enum_type_name() + }; + columns_enum_type_name.push(quote! { + Self::#field_name => #enum_type_name + }); } } } @@ -359,7 +529,10 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } let primary_key = { - let auto_increment = auto_increment && primary_keys.len() == 1; + let auto_increment = match auto_increment { + Some(value) => value && primary_keys.len() == 1, + None => primary_keys.len() == 1, + }; let primary_key_types = if primary_key_types.len() == 1 { let first = primary_key_types.first(); quote! { #first } @@ -384,7 +557,36 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } }; + let impl_model_ex = if model_ex { + quote!() + } else { + quote! { + impl Model { + #[doc = " Generated by sea-orm-macros"] + pub fn into_ex(self) -> Self { + self + } + } + } + }; + + let with_json_impls = { + #[cfg(feature = "with-json")] + quote! { + fn json_key(&self) -> &'static str { + match self { + #columns_json_keys + } + } + } + + #[cfg(not(feature = "with-json"))] + quote! {} + }; + Ok(quote! { + #impl_model_ex + #[doc = " Generated by sea-orm-macros"] #[derive(Copy, Clone, Debug, sea_orm::prelude::EnumIter, sea_orm::prelude::DeriveColumn)] pub enum Column { @@ -401,6 +603,12 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } } + fn enum_type_name(&self) -> Option<&'static str> { + match self { + #columns_enum_type_name + } + } + fn select_as(&self, expr: sea_orm::sea_query::Expr) -> sea_orm::sea_query::SimpleExpr { match self { #columns_select_as @@ -414,6 +622,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res _ => sea_orm::prelude::ColumnTrait::save_enum_as(self, val), } } + + #with_json_impls } #entity_def diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index 999db62feb..de511f4ed6 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -1,48 +1,205 @@ +use super::util::GetMeta; use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote, quote_spanned}; -use syn::{ext::IdentExt, Data, DataStruct, Field, Fields, Generics}; - -/// Method to derive a [QueryResult](sea_orm::QueryResult) -pub fn expand_derive_from_query_result( - ident: Ident, - data: Data, - generics: Generics, -) -> syn::Result { - let fields = match data { - Data::Struct(DataStruct { - fields: Fields::Named(named), +use quote::{ToTokens, format_ident, quote, quote_spanned}; +use syn::{ + Data, DataStruct, DeriveInput, Fields, Generics, Meta, ext::IdentExt, punctuated::Punctuated, + token::Comma, +}; + +#[derive(Debug)] +enum Error { + InputNotStruct, +} + +pub(super) enum ItemType { + Flat, + Skip, + Nested, +} + +pub(super) struct DeriveFromQueryResult { + pub ident: syn::Ident, + pub generics: Generics, + pub fields: Vec, +} + +pub(super) struct FromQueryResultItem { + pub typ: ItemType, + pub ident: Ident, + pub alias: Option, +} + +/// Initially, we try to obtain the value for each field and check if it is an ordinary DB error +/// (which we return immediatly), or a null error. +/// +/// ### Background +/// +/// Null errors do not necessarily mean that the deserialization as a whole fails, +/// since structs embedding the current one might have wrapped the current one in an `Option`. +/// In this case, we do not want to swallow other errors, which are very likely to actually be +/// programming errors that should be noticed (and fixed). +struct TryFromQueryResultCheck<'a>(bool, &'a FromQueryResultItem); + +impl ToTokens for TryFromQueryResultCheck<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let FromQueryResultItem { ident, typ, alias } = self.1; + + match typ { + ItemType::Flat => { + let name = alias + .to_owned() + .unwrap_or_else(|| ident.unraw().to_string()); + tokens.extend(quote! { + let #ident = match row.try_get_nullable(pre, #name) { + Err(v @ sea_orm::TryGetError::DbErr(_)) => { + return Err(v); + } + v => v, + }; + }); + } + ItemType::Skip => { + tokens.extend(quote! { + let #ident = std::default::Default::default(); + }); + } + ItemType::Nested => { + let prefix = if self.0 { + let name = ident.unraw().to_string(); + quote! { &format!("{pre}{}_", #name) } + } else { + quote! { pre } + }; + tokens.extend(quote! { + let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, #prefix) { + Err(v @ sea_orm::TryGetError::DbErr(_)) => { + return Err(v); + } + v => v, + }; + }); + } + } + } +} + +struct TryFromQueryResultAssignment<'a>(&'a FromQueryResultItem); + +impl ToTokens for TryFromQueryResultAssignment<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let FromQueryResultItem { ident, typ, .. } = self.0; + + match typ { + ItemType::Flat | ItemType::Nested => { + tokens.extend(quote! { + #ident: #ident?, + }); + } + ItemType::Skip => { + tokens.extend(quote! { + #ident, + }); + } + } + } +} + +impl DeriveFromQueryResult { + fn new( + DeriveInput { + ident, + data, + generics, .. - }) => named.named, - _ => { - return Ok(quote_spanned! { - ident.span() => compile_error!("you can only derive FromQueryResult on structs"); - }) + }: DeriveInput, + ) -> Result { + let parsed_fields = match data { + Data::Struct(DataStruct { + fields: Fields::Named(named), + .. + }) => named.named, + _ => return Err(Error::InputNotStruct), + }; + + let mut fields = Vec::with_capacity(parsed_fields.len()); + for parsed_field in parsed_fields { + let mut typ = ItemType::Flat; + let mut alias = None; + for attr in parsed_field.attrs.iter() { + if !attr.path().is_ident("sea_orm") { + continue; + } + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) + { + for meta in list.iter() { + if meta.exists("skip") { + typ = ItemType::Skip; + } else if meta.exists("nested") { + typ = ItemType::Nested; + } else if let Some(alias_) = meta.get_as_kv("from_alias") { + alias = Some(alias_); + } else { + alias = meta.get_as_kv("alias"); + } + } + } + } + let ident = format_ident!("{}", parsed_field.ident.unwrap().to_string()); + fields.push(FromQueryResultItem { typ, ident, alias }); } - }; - - let field: Vec = fields - .into_iter() - .map(|Field { ident, .. }| format_ident!("{}", ident.unwrap().to_string())) - .collect(); - - let name: Vec = field - .iter() - .map(|f| { - let s = f.unraw().to_string(); - quote! { #s } + + Ok(Self { + ident, + generics, + fields, }) - .collect(); + } - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + fn expand(&self) -> syn::Result { + Ok(self.impl_from_query_result(false)) + } - Ok(quote!( - #[automatically_derived] - impl #impl_generics sea_orm::FromQueryResult for #ident #ty_generics #where_clause { - fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { - Ok(Self { - #(#field: row.try_get(pre, #name)?),* - }) + pub(super) fn impl_from_query_result(&self, prefix: bool) -> TokenStream { + let Self { + ident, + generics, + fields, + } = self; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let ident_try_init: Vec<_> = fields + .iter() + .map(|s| TryFromQueryResultCheck(prefix, s)) + .collect(); + let ident_try_assign: Vec<_> = fields.iter().map(TryFromQueryResultAssignment).collect(); + + quote!( + #[automatically_derived] + impl #impl_generics sea_orm::FromQueryResult for #ident #ty_generics #where_clause { + fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { + Ok(Self::from_query_result_nullable(row, pre)?) + } + + fn from_query_result_nullable(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { + #(#ident_try_init)* + + Ok(Self { + #(#ident_try_assign)* + }) + } } - } - )) + ) + } +} + +pub fn expand_derive_from_query_result(input: DeriveInput) -> syn::Result { + let ident_span = input.ident.span(); + + match DeriveFromQueryResult::new(input) { + Ok(partial_model) => partial_model.expand(), + Err(Error::InputNotStruct) => Ok(quote_spanned! { + ident_span => compile_error!("you can only derive `FromQueryResult` on named struct"); + }), + } } diff --git a/sea-orm-macros/src/derives/into_active_model.rs b/sea-orm-macros/src/derives/into_active_model.rs index 2f7dc3f27b..4a0a6f2c03 100644 --- a/sea-orm-macros/src/derives/into_active_model.rs +++ b/sea-orm-macros/src/derives/into_active_model.rs @@ -1,26 +1,56 @@ -use bae::FromAttributes; -use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned}; - -/// Attributes to derive an ActiveModel -#[derive(Default, FromAttributes)] -pub struct SeaOrm { - pub active_model: Option, -} +use proc_macro2::TokenStream; +use quote::{format_ident, quote, quote_spanned}; +use syn::{Meta, PathArguments, PathSegment, punctuated::Punctuated, token::Comma}; + +use super::util::GetMeta; enum Error { InputNotStruct, Syn(syn::Error), } -struct IntoActiveModel { - attrs: SeaOrm, - fields: syn::punctuated::Punctuated, - field_idents: Vec, - ident: syn::Ident, +/// Matches all potential ways to convert struct fields into ActiveModel ones +pub(super) enum IntoActiveModelField { + /// `IntoActiveValue::into_active_value(self.field).into()` + Normal(syn::Ident), + /// Option with fallback: `Some(v) => Set(v).into(), None => Set(expr).into()` + WithDefault { ident: syn::Ident, expr: syn::Expr }, +} + +impl IntoActiveModelField { + pub(super) fn ident(&self) -> &syn::Ident { + match self { + IntoActiveModelField::Normal(ident) => ident, + IntoActiveModelField::WithDefault { ident, .. } => ident, + } + } +} + +/// Contains all the information extracted from the input struct and its attributes +/// needed to generate the `IntoActiveModel` trait implementation. +pub(super) struct DeriveIntoActiveModel { + /// The identifier of the input struct + pub ident: syn::Ident, + /// Optional explicit ActiveModel type specified via `#[sea_orm(active_model = "Type")]` + pub active_model: Option, + /// handles provided struct fields + pub fields: Vec, + /// handles fields set by #[sea_orm(set(field = expr))] + pub set_fields: Vec<(syn::Ident, syn::Expr)>, + /// require all fields specified, no `..default::Default()` + pub exhaustive: bool, } -impl IntoActiveModel { +impl DeriveIntoActiveModel { + /// This function finds attributes relevant for this macros: + /// Container attributes (#[sea_orm(...)]) on the struct for: + /// - active_model: explicit ActiveModel type + /// - exhaustive: require all fields to be set + /// - set(...): provided values for ommited fields + /// + /// Field attributes (#[sea_orm(...)]) with: + /// - ignore/skip: exclude from conversion + /// - default: fallback value for Option fields fn new(input: syn::DeriveInput) -> Result { let fields = match input.data { syn::Data::Struct(syn::DataStruct { @@ -30,22 +60,66 @@ impl IntoActiveModel { _ => return Err(Error::InputNotStruct), }; - let attrs = SeaOrm::try_from_attributes(&input.attrs) - .map_err(Error::Syn)? - .unwrap_or_default(); + let mut active_model = None; + let mut set_fields = Vec::new(); + let mut exhaustive = false; - let ident = input.ident; + for attr in input.attrs.iter() { + if !attr.path().is_ident("sea_orm") { + continue; + } + + // Parse container attributes: #[sea_orm(...)] + // Supports: + // - active_model = "Type": explicitly specify the ActiveModel type + // - exhaustive: require all ActiveModel fields to be explicitly set + // - set(field = expr, ...): provide default values for fields not in the input struct + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { + for meta in list { + // Parse active_model attribute: #[sea_orm(active_model = "MyActiveModel")] + if let Some(s) = meta.get_as_kv("active_model") { + active_model = Some(syn::parse_str::(&s).map_err(Error::Syn)?); + } + // Parse exhaustive flag: #[sea_orm(exhaustive)] + // When set, prevents using Default::default() for unspecified fields + if meta.exists("exhaustive") { + exhaustive = true; + } + // Parse set attribute: #[sea_orm(set(field1 = expr1, field2 = expr2, ...))] + // Collects field assignments to be included in the generated ActiveModel + if let Meta::List(meta_list) = &meta { + if meta_list.path.is_ident("set") { + let nested = meta_list + .parse_args_with(Punctuated::::parse_terminated) + .map_err(Error::Syn)?; + for nested_meta in nested { + if let Some(val) = nested_meta.get_as_kv_with_ident() { + let (ident, expr_str) = val; + let expr = syn::parse_str::(&expr_str) + .map_err(Error::Syn)?; + set_fields.push((ident, expr)); + } + } + } + } + } + } + } - let field_idents = fields - .iter() - .map(|field| field.ident.as_ref().unwrap().clone()) - .collect(); + // Field attributes + let mut field_idents: Vec = Vec::new(); + for field in fields.iter() { + if let Some(f) = parse_field(field)? { + field_idents.push(f); + } + } - Ok(IntoActiveModel { - attrs, - fields, - field_idents, - ident, + Ok(Self { + ident: input.ident, + active_model, + fields: field_idents, + set_fields, + exhaustive, }) } @@ -55,34 +129,81 @@ impl IntoActiveModel { Ok(expanded_impl_into_active_model) } - fn impl_into_active_model(&self) -> TokenStream { + /// Generates the implementation of `IntoActiveModel` trait for the input struct + pub(super) fn impl_into_active_model(&self) -> TokenStream { let Self { - attrs, ident, - field_idents, + active_model, fields, + set_fields, + exhaustive, } = self; - let active_model_ident = attrs - .active_model + let mut active_model_ident = active_model .clone() - .unwrap_or_else(|| syn::Ident::new("ActiveModel", Span::call_site())); + .unwrap_or_else(|| syn::parse_str::("ActiveModel").unwrap()); + + // Create a type alias for qualified types + let type_alias_definition = if is_qualified_type(&active_model_ident) { + let type_alias = format_ident!("ActiveModelFor{ident}"); + let type_def = quote!( type #type_alias = #active_model_ident; ); + active_model_ident = syn::Type::Path(syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: [PathSegment { + ident: type_alias, + arguments: PathArguments::None, + }] + .into_iter() + .collect(), + }, + }); + type_def + } else { + quote!() + }; + + let field_idents: Vec<_> = fields.iter().map(|f| f.ident()).collect(); - let expanded_fields_into_active_model = fields.iter().map(|field| { - let field_ident = field.ident.as_ref().unwrap(); + // Generate field conversion code based on field type + let expanded_fields = fields.iter().map(|field| match field { + IntoActiveModelField::Normal(ident) => quote!( + sea_orm::IntoActiveValue::<_>::into_active_value(self.#ident).into() + ), + IntoActiveModelField::WithDefault { ident, expr } => quote!({ + match self.#ident.into() { + Some(v) => sea_orm::ActiveValue::Set(v).into(), + None => sea_orm::ActiveValue::Set(#expr).into(), + } + }), + }); + // Add custom field assignments from #[sea_orm(set(field = expr))] + let (set_idents, set_exprs): (Vec<_>, Vec<_>) = set_fields.iter().cloned().unzip(); + let expanded_sets = set_exprs.iter().map(|expr| { quote!( - sea_orm::IntoActiveValue::<_>::into_active_value(self.#field_ident).into() + sea_orm::ActiveValue::Set(#expr) ) }); + // Add defaults(Unset) unless exhaustive mode is enabled + let rest = if *exhaustive { + quote!() + } else { + quote!(..::std::default::Default::default()) + }; + quote!( + #type_alias_definition + #[automatically_derived] impl sea_orm::IntoActiveModel<#active_model_ident> for #ident { fn into_active_model(self) -> #active_model_ident { #active_model_ident { - #( #field_idents: #expanded_fields_into_active_model, )* - ..::std::default::Default::default() + #( #field_idents: #expanded_fields, )* + #( #set_idents: #expanded_sets, )* + #rest } } } @@ -90,11 +211,69 @@ impl IntoActiveModel { } } +/// Parse field-level attributes on each struct field +/// Supports: +/// - ignore or skip: exclude the field from conversion +/// - default = "expr": provide a fallback value for Option fields (Some(v) => Set(v), None => Set(expr)) +fn parse_field(field: &syn::Field) -> Result, Error> { + let ident = field.ident.as_ref().unwrap().clone(); + // Default expression for this field + let mut default_expr: Option = None; + + for attr in field.attrs.iter() { + if !attr.path().is_ident("sea_orm") { + continue; + } + + // Parse the attribute arguments: #[sea_orm(...)] + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { + for meta in list.iter() { + // Check for ignore/skip: #[sea_orm(ignore)] or #[sea_orm(skip)] + if meta.exists("ignore") || meta.exists("skip") { + return Ok(None); + } + // Check for bare default: #[sea_orm(default)] + if meta.exists("default") { + if default_expr.is_some() { + return Err(Error::Syn(syn::Error::new_spanned( + meta, + "duplicate `default` attribute", + ))); + } + let expr: syn::Expr = syn::parse_quote!(::core::default::Default::default()); + default_expr = Some(expr); + continue; // Skip next default check + } + // Check for default value: #[sea_orm(default = "expr")] + if let Some(expr_str) = meta.get_as_kv("default") { + // Error on duplicate `default` + if default_expr.is_some() { + return Err(Error::Syn(syn::Error::new_spanned( + meta, + "duplicate `default` attribute", + ))); + } + // Parse the expression string into a syn::Expr + let expr = syn::parse_str::(&expr_str).map_err(Error::Syn)?; + default_expr = Some(expr); + } + } + } + } + + // Finnaly match and return appropriate field type + if let Some(expr) = default_expr { + Ok(Some(IntoActiveModelField::WithDefault { ident, expr })) + } else { + Ok(Some(IntoActiveModelField::Normal(ident))) + } +} + /// Method to derive the ActiveModel from the [ActiveModelTrait](sea_orm::ActiveModelTrait) pub fn expand_into_active_model(input: syn::DeriveInput) -> syn::Result { let ident_span = input.ident.span(); - match IntoActiveModel::new(input) { + match DeriveIntoActiveModel::new(input) { Ok(model) => model.expand(), Err(Error::InputNotStruct) => Ok(quote_spanned! { ident_span => compile_error!("you can only derive IntoActiveModel on structs"); @@ -102,3 +281,7 @@ pub fn expand_into_active_model(input: syn::DeriveInput) -> syn::Result Err(err), } } + +fn is_qualified_type(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Path(syn::TypePath { qself: Some(_), .. })) +} diff --git a/sea-orm-macros/src/derives/mod.rs b/sea-orm-macros/src/derives/mod.rs index 29f69b053a..156d0937e0 100644 --- a/sea-orm-macros/src/derives/mod.rs +++ b/sea-orm-macros/src/derives/mod.rs @@ -1,33 +1,50 @@ mod active_enum; +mod active_enum_display; mod active_model; mod active_model_behavior; +mod active_model_ex; +mod arrow_schema; mod attributes; +mod case_style; mod column; +mod derive_iden; mod entity; +mod entity_loader; mod entity_model; mod from_query_result; mod into_active_model; mod migration; mod model; +mod model_ex; mod partial_model; mod primary_key; mod related_entity; mod relation; mod try_getable_from_json; +mod typed_column; mod util; +mod value_type; +mod value_type_match; pub use active_enum::*; +pub use active_enum_display::*; pub use active_model::*; pub use active_model_behavior::*; +pub use active_model_ex::*; +pub use arrow_schema::*; pub use column::*; +pub use derive_iden::*; pub use entity::*; pub use entity_model::*; pub use from_query_result::*; pub use into_active_model::*; pub use migration::*; pub use model::*; +pub use model_ex::*; pub use partial_model::*; pub use primary_key::*; pub use related_entity::*; pub use relation::*; pub use try_getable_from_json::*; +pub use typed_column::*; +pub use value_type::*; diff --git a/sea-orm-macros/src/derives/model.rs b/sea-orm-macros/src/derives/model.rs index d3813f28b7..31e7e66c11 100644 --- a/sea-orm-macros/src/derives/model.rs +++ b/sea-orm-macros/src/derives/model.rs @@ -3,39 +3,38 @@ use super::{ util::{escape_rust_keyword, field_not_ignored, trim_starting_raw_identifier}, }; use heck::ToUpperCamelCase; +use itertools::izip; use proc_macro2::TokenStream; -use quote::{format_ident, quote, quote_spanned}; +use quote::{format_ident, quote}; use std::iter::FromIterator; -use syn::{punctuated::Punctuated, token::Comma, Ident, Lit, Meta}; +use syn::{Attribute, Data, Expr, Ident, LitStr}; -enum Error { - InputNotStruct, - Syn(syn::Error), -} - -struct DeriveModel { - column_idents: Vec, - entity_ident: syn::Ident, - field_idents: Vec, - ident: syn::Ident, +pub(crate) struct DeriveModel { + column_idents: Vec, + entity_ident: Ident, + field_idents: Vec, + field_types: Vec, + ident: Ident, ignore_attrs: Vec, } impl DeriveModel { - fn new(input: syn::DeriveInput) -> Result { - let fields = match input.data { + pub fn new(ident: &Ident, data: &Data, attrs: &[Attribute]) -> syn::Result { + let fields = match data { syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(syn::FieldsNamed { named, .. }), .. }) => named, - _ => return Err(Error::InputNotStruct), + _ => { + return Err(syn::Error::new_spanned( + ident, + "You can only derive DeriveModel on structs", + )); + } }; - let sea_attr = derive_attr::SeaOrm::try_from_attributes(&input.attrs) - .map_err(Error::Syn)? - .unwrap_or_default(); + let sea_attr = derive_attr::SeaOrm::try_from_attributes(attrs)?.unwrap_or_default(); - let ident = input.ident; let entity_ident = sea_attr.entity.unwrap_or_else(|| format_ident!("Entity")); let field_idents = fields @@ -43,6 +42,8 @@ impl DeriveModel { .map(|field| field.ident.as_ref().unwrap().clone()) .collect(); + let field_types = fields.iter().map(|field| field.ty.clone()).collect(); + let column_idents = fields .iter() .map(|field| { @@ -50,33 +51,29 @@ impl DeriveModel { let ident = trim_starting_raw_identifier(ident).to_upper_camel_case(); let ident = escape_rust_keyword(ident); let mut ident = format_ident!("{}", &ident); - for attr in field.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { - continue; - } - if let Ok(list) = - attr.parse_args_with(Punctuated::::parse_terminated) - { - for meta in list.iter() { - if let Meta::NameValue(nv) = meta { - if let Some(name) = nv.path.get_ident() { - if name == "enum_name" { - if let Lit::Str(litstr) = &nv.lit { - ident = syn::parse_str(&litstr.value()).unwrap(); - } - } - } + field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("enum_name") { + ident = syn::parse_str(&meta.value()?.parse::()?.value()) + .unwrap(); + } else { + // Reads the value expression to advance the parse stream. + // Some parameters, such as `primary_key`, do not have any value, + // so ignoring an error occurred here. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); } - } - } - } - ident + + Ok(()) + }) + })?; + + Ok(ident) }) - .collect(); + .collect::>()?; let ignore_attrs = fields .iter() @@ -87,7 +84,8 @@ impl DeriveModel { column_idents, entity_ident, field_idents, - ident, + field_types, + ident: ident.clone(), ignore_attrs, }) } @@ -106,44 +104,96 @@ impl DeriveModel { let ident = &self.ident; let field_idents = &self.field_idents; let column_idents = &self.column_idents; - let field_values: Vec = column_idents - .iter() - .zip(&self.ignore_attrs) - .map(|(column_ident, ignore)| { - if *ignore { - quote! { - Default::default() - } - } else { - quote! { - row.try_get(pre, sea_orm::IdenStatic::as_str(&<::Entity as sea_orm::entity::EntityTrait>::Column::#column_ident).into())? + let field_types = &self.field_types; + let ignore_attrs = &self.ignore_attrs; + + let (field_readers, field_values): (Vec, Vec) = izip!( + field_idents.iter(), + column_idents, + field_types, + ignore_attrs, + ) + .map(|(field_ident, column_ident, field_type, &ignore)| { + if ignore { + let reader = quote! { + let #field_ident: Option<()> = None; + }; + let unwrapper = quote! { + #field_ident: Default::default() + }; + (reader, unwrapper) + } else { + let reader = quote! { + let #field_ident = + row.try_get_nullable::>( + pre, + sea_orm::IdenStatic::as_str( + &<::Entity + as sea_orm::entity::EntityTrait>::Column::#column_ident + ).into() + )?; + }; + let unwrapper = quote! { + #field_ident: #field_ident.ok_or_else(|| sea_orm::DbErr::Type( + format!( + "Missing value for column '{}'", + sea_orm::IdenStatic::as_str( + &<::Entity + as sea_orm::entity::EntityTrait>::Column::#column_ident + ) + ) + ))? + }; + (reader, unwrapper) + } + }) + .unzip(); + + // When a nested model is loaded via LEFT JOIN, all its fields may be NULL. + // In that case we interpret it as "no nested row" (i.e., Option::None). + // This check detects that condition by testing if all non-ignored fields are NULL. + let all_null_check = { + let checks: Vec<_> = izip!(field_idents, ignore_attrs) + .filter_map(|(field_ident, &ignore)| { + if ignore { + None + } else { + Some(quote! { #field_ident.is_none() }) } - } - }) - .collect(); + }) + .collect(); + + quote! { true #( && #checks )* } + }; quote!( #[automatically_derived] impl sea_orm::FromQueryResult for #ident { fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { + Self::from_query_result_nullable(row, pre).map_err(Into::into) + } + + fn from_query_result_nullable(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { + #(#field_readers)* + + if #all_null_check { + return Err(sea_orm::TryGetError::Null("All fields of nested model are null".into())); + } + Ok(Self { - #(#field_idents: #field_values),* + #(#field_values),* }) } } ) } - fn impl_model_trait<'a>(&'a self) -> TokenStream { + pub fn impl_model_trait<'a>(&'a self) -> TokenStream { let ident = &self.ident; let entity_ident = &self.entity_ident; let ignore_attrs = &self.ignore_attrs; let ignore = |(ident, ignore): (&'a Ident, &bool)| -> Option<&'a Ident> { - if *ignore { - None - } else { - Some(ident) - } + if *ignore { None } else { Some(ident) } }; let field_idents: Vec<&Ident> = self .field_idents @@ -157,6 +207,18 @@ impl DeriveModel { .zip(ignore_attrs) .filter_map(ignore) .collect(); + let get_field_type: Vec = self + .field_types + .iter() + .zip(ignore_attrs) + .filter_map(|(ty, ignore)| { + if *ignore { + None + } else { + Some(quote!(<#ty as sea_orm::sea_query::ValueType>::array_type())) + } + }) + .collect(); let missing_field_msg = format!("field does not exist on {ident}"); @@ -168,30 +230,31 @@ impl DeriveModel { fn get(&self, c: ::Column) -> sea_orm::Value { match c { #(::Column::#column_idents => self.#field_idents.clone().into(),)* - _ => panic!(#missing_field_msg), } } - fn set(&mut self, c: ::Column, v: sea_orm::Value) { + fn get_value_type(c: ::Column) -> sea_orm::sea_query::ArrayType { match c { - #(::Column::#column_idents => self.#field_idents = v.unwrap(),)* - _ => panic!(#missing_field_msg), + #(::Column::#column_idents => #get_field_type,)* } } + + fn try_set(&mut self, c: ::Column, v: sea_orm::Value) -> Result<(), sea_orm::DbErr> { + match c { + #(::Column::#column_idents => self.#field_idents = sea_orm::sea_query::ValueType::try_from(v).map_err(|e| sea_orm::DbErr::Type(e.to_string()))?,)* + _ => return Err(sea_orm::DbErr::Type(#missing_field_msg.to_owned())), + } + Ok(()) + } } ) } } -/// Method to derive an ActiveModel -pub fn expand_derive_model(input: syn::DeriveInput) -> syn::Result { - let ident_span = input.ident.span(); - - match DeriveModel::new(input) { - Ok(model) => model.expand(), - Err(Error::InputNotStruct) => Ok(quote_spanned! { - ident_span => compile_error!("you can only derive DeriveModel on structs"); - }), - Err(Error::Syn(err)) => Err(err), - } +pub fn expand_derive_model( + ident: &Ident, + data: &Data, + attrs: &[Attribute], +) -> syn::Result { + DeriveModel::new(ident, data, attrs)?.expand() } diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs new file mode 100644 index 0000000000..ca522fd8a2 --- /dev/null +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -0,0 +1,862 @@ +use super::attributes::compound_attr; +use super::entity_loader::{EntityLoaderField, EntityLoaderSchema, expand_entity_loader}; +use super::util::{extract_compound_entity, format_field_ident, is_compound_field}; +use super::{expand_typed_column, model::DeriveModel}; +use heck::ToUpperCamelCase; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; +use std::collections::{BTreeMap, HashMap}; +use syn::{ + Attribute, Data, Expr, Fields, ItemStruct, Lit, Meta, Type, parse_quote, + punctuated::Punctuated, token::Comma, +}; + +pub fn expand_sea_orm_model(input: ItemStruct, compact: bool) -> syn::Result { + let model = input.ident; + let vis = input.vis; + let mut all_fields = input.fields; + + let mut model_attrs: Vec = Vec::new(); + let mut model_ex_attrs: Vec = Vec::new(); + let mut has_arrow_schema = false; + + for attr in input.attrs { + if !attr.path().is_ident("sea_orm") { + model_attrs.push(attr.clone()); + model_ex_attrs.push(attr); + continue; + } + + let mut other_attrs = Punctuated::::new(); + + attr.parse_nested_meta(|meta| { + let is_model = meta.path.is_ident("model_attrs"); + let is_model_ex = meta.path.is_ident("model_ex_attrs"); + + if is_model || is_model_ex { + let content; + syn::parenthesized!(content in meta.input); + use syn::parse::Parse; + let nested_metas = content.parse_terminated(Meta::parse, Comma)?; + for m in nested_metas { + let new_attr: Attribute = parse_quote!( #[#m] ); + if is_model { + model_attrs.push(new_attr); + } else { + model_ex_attrs.push(new_attr); + } + } + } else if meta.path.is_ident("arrow_schema") { + has_arrow_schema = true; + } else { + let path = &meta.path; + if meta.input.peek(syn::Token![=]) { + let value: Expr = meta.value()?.parse()?; + other_attrs.push(parse_quote!( #path = #value )); + } else if meta.input.is_empty() || meta.input.peek(Comma) { + other_attrs.push(parse_quote!( #path )); + } else { + let content; + syn::parenthesized!(content in meta.input); + let tokens: TokenStream = content.parse()?; + other_attrs.push(parse_quote!( #path(#tokens) )); + } + } + Ok(()) + })?; + + if !other_attrs.is_empty() { + let attr: Attribute = parse_quote!( #[sea_orm(#other_attrs)] ); + model_attrs.push(attr.clone()); + model_ex_attrs.push(attr); + } + } + + if has_arrow_schema { + model_attrs.push(parse_quote!(#[derive(DeriveArrowSchema)])); + } + + let model_ex = Ident::new(&format!("{model}Ex"), model.span()); + for attr in &mut model_ex_attrs { + if attr.path().is_ident("derive") { + if let Meta::List(list) = &mut attr.meta { + let mut new_list: Punctuated<_, Comma> = Punctuated::new(); + + list.parse_nested_meta(|meta| { + if meta.path.is_ident("Eq") { + // skip + } else if meta.path.is_ident("DeriveEntityModel") { + // replace macro + new_list.push(parse_quote!(DeriveModelEx)); + new_list.push(parse_quote!(DeriveActiveModelEx)); + } else { + new_list.push(meta.path); + } + + Ok(()) + })?; + + *attr = parse_quote!(#[derive( #new_list )]); + } + } + } + + let compact_model = if compact { + quote!(#[sea_orm(compact_model)]) + } else { + quote!() + }; + + let mut model_fields = Vec::new(); + + for field in all_fields.iter_mut() { + let field_type = &field.ty; + let field_type = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + if is_compound_field(&field_type) { + let entity_path = extract_compound_entity(&field_type); + if field_type.starts_with("Option<") || field_type.starts_with("HasOne<") { + field.ty = syn::parse_str(&format!("HasOne < {entity_path} >"))?; + } else { + field.ty = syn::parse_str(&format!("HasMany < {entity_path} >"))?; + } + } else { + model_fields.push(field); + } + } + + Ok(quote! { + #(#model_attrs)* + #[sea_orm(model_ex)] + #vis struct #model { + #(#model_fields),* + } + + #(#model_ex_attrs)* + #compact_model + #vis struct #model_ex #all_fields + }) +} + +pub fn expand_derive_model_ex( + ident: Ident, + data: Data, + attrs: Vec, +) -> syn::Result { + let mut compact = false; + let mut model_fields: Vec = Vec::new(); + let mut compound_fields: Vec = Vec::new(); + let mut impl_related = Vec::new(); + let mut entity_loader_schema = EntityLoaderSchema::default(); + let mut unique_keys = BTreeMap::new(); + + attrs + .iter() + .filter(|attr| attr.path().is_ident("sea_orm")) + .try_for_each(|attr| { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("compact_model") { + compact = true; + } else { + // Reads the value expression to advance the parse stream. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + Ok(()) + }) + })?; + + if let Data::Struct(item_struct) = &data { + if let Fields::Named(fields) = &item_struct.fields { + for field in &fields.named { + if let Some(ident) = &field.ident { + let field_type = &field.ty; + let field_type = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + if is_compound_field(&field_type) { + let compound_attrs = + compound_attr::SeaOrm::from_attributes(&field.attrs).ok(); + let is_reverse = compound_attrs + .as_ref() + .map(|r| r.reverse.is_some()) + .unwrap_or_default(); + let relation_enum = compound_attrs + .as_ref() + .and_then(|r| r.relation_enum.clone()); + if field_type.starts_with("HasOne<") { + entity_loader_schema.fields.push(EntityLoaderField { + is_one: true, + is_self: field_type == "HasOne", + is_reverse, + field: ident.clone(), + entity: extract_compound_entity(&field_type).to_owned(), + relation_enum, + via: None, + }); + } else if field_type.starts_with("HasMany<") { + entity_loader_schema.fields.push(EntityLoaderField { + is_one: false, + is_self: field_type == "HasMany", + is_reverse, + field: ident.clone(), + entity: extract_compound_entity(&field_type).to_owned(), + relation_enum, + via: compound_attrs.as_ref().and_then(|r| r.via.clone()), + }); + } + if let Some(attrs) = compound_attrs { + if compact + && (attrs.has_one.is_some() + || attrs.has_many.is_some() + || attrs.belongs_to.is_some()) + { + return Err(syn::Error::new_spanned( + ident, + "You cannot use #[has_one / has_many / belongs_to] on #[sea_orm::compact_model], please use #[sea_orm::model] instead.", + )); + } else if attrs.belongs_to.is_some() + && !field_type.starts_with("HasOne<") + { + return Err(syn::Error::new_spanned( + ident, + "belongs_to must be paired with HasOne", + )); + } else if attrs.has_one.is_some() && !field_type.starts_with("HasOne<") + { + return Err(syn::Error::new_spanned( + ident, + "has_one must be paired with HasOne", + )); + } else if attrs.has_many.is_some() + && !field_type.starts_with("HasMany<") + { + return Err(syn::Error::new_spanned( + ident, + "has_many must be paired with HasMany", + )); + } + impl_related.push((attrs, field_type)); + } + compound_fields.push(format_field_ident(field)); + } else { + // scalar field + for attr in field.attrs.iter() { + // still have to parse column attributes to extract unique keys + if attr.path().is_ident("sea_orm") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("unique") { + unique_keys.insert( + ident.clone(), + vec![(ident.clone(), field.ty.clone())], + ); + } else if meta.path.is_ident("unique_key") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + unique_keys + .entry(litstr.parse()?) + .or_default() + .push((ident.clone(), field.ty.clone())); + } else { + return Err( + meta.error(format!("Invalid unique_key {lit:?}")) + ); + } + } else { + // Reads the value expression to advance the parse stream. + let _: Option = + meta.value().and_then(|v| v.parse()).ok(); + } + + Ok(()) + })?; + } + } + model_fields.push(format_field_ident(field)); + } + } + } + } + } + + let impl_model_trait = DeriveModel::new(&ident, &data, &attrs)?.impl_model_trait(); + + let impl_from_model = quote! { + impl Model { + #[doc = " Generated by sea-orm-macros"] + pub fn into_ex(self) -> ModelEx { + self.into() + } + } + + #[automatically_derived] + impl std::convert::From for ModelEx { + fn from(m: Model) -> Self { + Self { + #(#model_fields: m.#model_fields,)* + #(#compound_fields: Default::default(),)* + } + } + } + + #[automatically_derived] + impl std::convert::From for Model { + fn from(m: ModelEx) -> Self { + Self { + #(#model_fields: m.#model_fields,)* + } + } + } + + #[automatically_derived] + impl PartialEq for Model { + fn eq(&self, other: &ModelEx) -> bool { + true #(&& self.#model_fields == other.#model_fields)* + } + } + + #[automatically_derived] + impl PartialEq for ModelEx { + fn eq(&self, other: &Model) -> bool { + true #(&& self.#model_fields == other.#model_fields)* + } + } + }; + + let mut relation_enum_variants: Punctuated<_, Comma> = Punctuated::new(); + let mut related_entity_enum_variants: Punctuated<_, Comma> = Punctuated::new(); + + let impl_related_trait = { + let mut ts = TokenStream::new(); + let mut seen_entity = HashMap::new(); + for (_, field_type) in impl_related.iter() { + let entity_path = extract_compound_entity(field_type); + *seen_entity.entry(entity_path).or_insert(0) += 1; + } + + for (attrs, field_type) in impl_related.iter() { + if attrs.self_ref.is_some() && attrs.via.is_some() { + ts.extend(expand_impl_related_self_via(attrs, field_type)?); + } else { + if attrs.self_ref.is_some() && attrs.relation_enum.is_none() { + return Err(syn::Error::new_spanned( + ident, + "Please specify `relation_enum` for `self_ref`", + )); + } + if let Some(var) = relation_enum_variant(attrs, field_type) { + relation_enum_variants.push(var); + } + if attrs.self_ref.is_some() && field_type.starts_with("HasMany<") { + // related entity is already provided by the HasOne item + // so self_ref HasMany has to be skipped + continue; + } + let (first, second) = related_entity_enum_variant(attrs, field_type); + related_entity_enum_variants.push(first); + if let Some(second) = second { + related_entity_enum_variants.push(second); + } + let entity_path = extract_compound_entity(field_type); + if *seen_entity.get(entity_path).unwrap() == 1 { + // prevent impl trait for same entity twice + ts.extend(expand_impl_related_trait(attrs, field_type)?); + } + } + } + + ts + }; + + let relation_enum = if !compact { + quote! { + #[doc = " Generated by sea-orm-macros"] + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation { + #relation_enum_variants + } + } + } else { + // for backwards compatibility with compact models + quote!() + }; + + let related_entity_enum = if !compact { + quote! { + #[doc = " Generated by sea-orm-macros"] + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] + pub enum RelatedEntity { + #related_entity_enum_variants + } + } + } else { + // for backwards compatibility with compact models + quote!() + }; + + let (typed_column, typed_column_const) = expand_typed_column(&data)?; + + let (entity_find_by_key, loader_filter_by_key) = expand_find_by_unique_key(unique_keys); + + let entity_loader = expand_entity_loader(entity_loader_schema); + + Ok(quote! { + #typed_column + + #typed_column_const + + #impl_from_model + + #impl_model_trait + + #relation_enum + + #impl_related_trait + + #related_entity_enum + + #entity_loader + + impl Entity { + #[doc = " Generated by sea-orm-macros"] + pub const COLUMN: TypedColumn = COLUMN; + + #entity_find_by_key + } + + impl EntityLoader { + #loader_filter_by_key + } + }) +} + +fn relation_enum_variant(attr: &compound_attr::SeaOrm, ty: &str) -> Option { + let (related_entity, relation_enum) = get_related(attr, ty); + if attr.belongs_to.is_some() { + let belongs_to = Ident::new("belongs_to", Span::call_site()); + + let from = format_tuple( + "", + "Column", + &attr + .from + .as_ref() + .expect("Must specify `from` and `to` on belongs_to relation") + .value(), + ); + let to = format_tuple( + related_entity.trim_end_matches("::Entity"), + "Column", + &attr + .to + .as_ref() + .expect("Must specify `from` and `to` on belongs_to relation") + .value(), + ); + let mut extra: Punctuated<_, Comma> = Punctuated::new(); + if let Some(on_update) = &attr.on_update { + let tag = Ident::new("on_update", on_update.span()); + extra.push(quote!(#tag = #on_update)) + } + if let Some(on_delete) = &attr.on_delete { + let tag = Ident::new("on_delete", on_delete.span()); + extra.push(quote!(#tag = #on_delete)) + } + if let Some(()) = &attr.skip_fk { + extra.push(quote!(skip_fk)) + } + + Some(quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(#belongs_to = #related_entity, from = #from, to = #to, #extra)] + #relation_enum + }) + } else if attr.self_ref.is_some() + && attr.via.is_none() + && attr.from.is_some() + && attr.to.is_some() + { + let belongs_to = Ident::new("belongs_to", Span::call_site()); + + let from = format_tuple("", "Column", &attr.from.as_ref().unwrap().value()); + let to = format_tuple("", "Column", &attr.to.as_ref().unwrap().value()); + let mut extra: Punctuated<_, Comma> = Punctuated::new(); + if let Some(on_update) = &attr.on_update { + let tag = Ident::new("on_update", on_update.span()); + extra.push(quote!(#tag = #on_update)) + } + if let Some(on_delete) = &attr.on_delete { + let tag = Ident::new("on_delete", on_delete.span()); + extra.push(quote!(#tag = #on_delete)) + } + if let Some(()) = &attr.skip_fk { + extra.push(quote!(skip_fk)) + } + + Some(quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(#belongs_to = "Entity", from = #from, to = #to, #extra)] + #relation_enum + }) + } else if attr.self_ref.is_some() + && attr.via.is_none() + && attr.relation_reverse.is_some() + && ty.starts_with("HasMany<") + { + let has_many = Ident::new("has_many", Span::call_site()); + + #[allow(clippy::unnecessary_unwrap)] + let via_rel = format!( + "Relation::{}", + attr.relation_reverse.as_ref().unwrap().value() + ); + + Some(quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(#has_many = "Entity", via_rel = #via_rel)] + #relation_enum + }) + } else if attr.has_many.is_some() && attr.via.is_none() { + // skip junction relation + + let has_many = Ident::new("has_many", Span::call_site()); + let mut extra: Punctuated<_, Comma> = Punctuated::new(); + if let Some(via_rel) = &attr.via_rel { + let tag = Ident::new("via_rel", via_rel.span()); + let via_rel = format!("Relation::{}", via_rel.value()); + extra.push(quote!(#tag = #via_rel)) + } + + Some(quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(#has_many = #related_entity, #extra)] + #relation_enum + }) + } else if attr.has_one.is_some() { + let has_one = Ident::new("has_one", Span::call_site()); + let mut extra: Punctuated<_, Comma> = Punctuated::new(); + if let Some(via_rel) = &attr.via_rel { + let tag = Ident::new("via_rel", via_rel.span()); + let via_rel = format!("Relation::{}", via_rel.value()); + extra.push(quote!(#tag = #via_rel)) + } + + Some(quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(#has_one = #related_entity, #extra)] + #relation_enum + }) + } else { + None + } +} + +fn related_entity_enum_variant( + attr: &compound_attr::SeaOrm, + ty: &str, +) -> (TokenStream, Option) { + let (related_entity, relation_enum) = get_related(attr, ty); + + let extra = if attr.relation_enum.is_some() { + let relation_def = format!("Relation::{relation_enum}.def()"); + quote!(, def = #relation_def) + } else { + quote!() + }; + + let first = quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(entity = #related_entity #extra)] + #relation_enum + }; + let second = if attr.self_ref.is_some() { + let relation_def = format!("Relation::{relation_enum}.def().rev()"); + let relation_enum_ref = if let Some(relation_reverse) = &attr.relation_reverse { + Ident::new(&relation_reverse.value(), relation_reverse.span()) + } else { + Ident::new(&format!("{relation_enum}Reverse"), relation_enum.span()) + }; + Some(quote! { + #[doc = " Generated by sea-orm-macros"] + #[sea_orm(entity = #related_entity def = #relation_def)] + #relation_enum_ref + }) + } else { + None + }; + + (first, second) +} + +fn expand_impl_related_trait(attr: &compound_attr::SeaOrm, ty: &str) -> syn::Result { + if attr.has_one.is_some() || attr.has_many.is_some() || attr.belongs_to.is_some() { + let (related_entity, relation_enum) = get_related(attr, ty); + let related_entity: TokenStream = related_entity.parse().unwrap(); + + if let Some(via_lit) = &attr.via { + let via = via_lit.value(); + let mut junction = via.as_str(); + let mut via_related = ""; + if let Some((prefix, suffix)) = via.split_once("::") { + junction = prefix; + via_related = suffix; + } + let junction = Ident::new(junction, via_lit.span()); + let relation_def = quote!(super::#junction::Relation::#relation_enum.def()); + let via_relation_def: TokenStream = if !via_related.is_empty() { + let via_related = Ident::new(via_related, via_lit.span()); + quote!(super::#junction::Relation::#via_related.def().rev()) + } else { + quote!(>::to().rev()) + }; + + Ok(quote! { + #[doc = " Generated by sea-orm-macros"] + impl Related<#related_entity> for Entity { + fn to() -> RelationDef { + #relation_def + } + fn via() -> Option { + Some(#via_relation_def) + } + } + }) + + // #[sea_orm(relation, via = "cakes_bakers::Cake")] + // impl Related for Entity { + // fn to() -> RelationDef { + // super::cakes_bakers::Relation::Baker.def() + // } + // fn via() -> Option { + // Some(super::cakes_bakers::Relation::Cake.def().rev()) + // } + // } + } else { + let relation_def = quote!(Relation::#relation_enum.def()); + + Ok(quote! { + #[doc = " Generated by sea-orm-macros"] + impl Related<#related_entity> for Entity { + fn to() -> RelationDef { + #relation_def + } + } + }) + + // #[sea_orm(relation)] + // impl Related for Entity { + // fn to() -> RelationDef { + // Relation::Bakery.def() + // } + // } + } + } else { + Ok(quote!()) + } +} + +fn expand_impl_related_self_via( + attr: &compound_attr::SeaOrm, + ty: &str, +) -> syn::Result { + let Some(via) = &attr.via else { + return Err(syn::Error::new( + Span::call_site(), + "Please specify the junction Entity `via` for `self_ref`.", + )); + }; + + if ty != "HasMany" { + return Err(syn::Error::new_spanned( + via, + "self_ref + via field type must be `HasMany`", + )); + } + + if attr.reverse.is_some() { + return Ok(quote!()); + } + + if let (Some(from), Some(to)) = (&attr.from, &attr.to) { + let junction = Ident::new(&via.value(), via.span()); + let from = Ident::new(&from.value(), from.span()); + let to = Ident::new(&to.value(), to.span()); + + Ok(quote! { + #[doc = " Generated by sea-orm-macros"] + impl RelatedSelfVia for Entity { + fn to() -> RelationDef { + super::#junction::Relation::#to.def() + } + fn via() -> RelationDef { + super::#junction::Relation::#from.def().rev() + } + } + }) + + // #[sea_orm(self_ref, via = "user_follower", from = "User", to = "Follower")] + // impl RelatedSelfVia for Entity { + // fn to() -> RelationDef { + // super::user_follower::Relation::Follower.def() + // } + + // fn via() -> RelationDef { + // super::user_follower::Relation::User.def().rev() + // } + // } + } else { + Ok(quote!()) + } +} + +fn get_related<'a>(attr: &compound_attr::SeaOrm, ty: &'a str) -> (&'a str, Ident) { + let related_entity = extract_compound_entity(ty); + let relation_enum = if let Some(relation_enum) = &attr.relation_enum { + Ident::new( + &relation_enum.value().to_upper_camel_case(), + relation_enum.span(), + ) + } else { + Ident::new( + &infer_relation_name_from_entity(related_entity).to_upper_camel_case(), + Span::call_site(), + ) + }; + (related_entity, relation_enum) +} + +fn infer_relation_name_from_entity(s: &str) -> &str { + let s = s.trim_end_matches("::Entity"); + if let Some((_, suffix)) = s.rsplit_once("::") { + return suffix; + } + s +} + +fn expand_find_by_unique_key( + unique_keys: BTreeMap>, +) -> (TokenStream, TokenStream) { + let mut entity_find_by_key = TokenStream::new(); + let mut loader_filter_by_key = TokenStream::new(); + + for (name, columns) in unique_keys { + let find_method = format_ident!("find_by_{}", name); + let filter_method = format_ident!("filter_by_{}", name); + let delete_method = format_ident!("delete_by_{}", name); + if columns.len() > 1 { + let key_type = columns.iter().map(|(_, ty)| ty).collect::>(); + + let filters = columns + .iter() + .enumerate() + .map(|(i, (col, _))| { + let i = syn::Index::from(i); + let col = to_upper_camel_case(col); + quote!(Column::#col.eq(v.#i)) + }) + .collect::>(); + + entity_find_by_key.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #find_method(v: (#(#key_type),*)) -> Select { + Self::find() + #(.filter(#filters))* + } + + #[doc = " Generated by sea-orm-macros"] + pub fn #delete_method(v: (#(#key_type),*)) -> sea_orm::ValidatedDeleteOne { + sea_orm::Delete::_one_only_for_use_by_model_ex(Entity) + #(.filter(#filters))* + } + }); + loader_filter_by_key.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #filter_method(mut self, v: (#(#key_type),*)) -> Self { + #(self.filter_mut(#filters);)* + self + } + }); + } else { + let col = to_upper_camel_case(&columns[0].0); + let key_type = &columns[0].1; + entity_find_by_key.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #find_method(v: impl Into<#key_type>) -> Select { + Self::find().filter(Column::#col.eq(v.into())) + } + }); + loader_filter_by_key.extend(quote! { + #[doc = " Generated by sea-orm-macros"] + pub fn #filter_method(mut self, v: impl Into<#key_type>) -> Self { + self.filter_mut(Column::#col.eq(v.into())); + self + } + }); + } + } + + (entity_find_by_key, loader_filter_by_key) +} + +fn format_tuple(prefix: &str, middle: &str, suffix: &str) -> String { + use std::fmt::Write; + + let parts = if suffix.starts_with('(') && suffix.ends_with(')') { + suffix[1..suffix.len() - 1] + .split(',') + .map(|s| s.trim()) + .collect() + } else { + vec![suffix] + }; + + let mut output = String::new(); + if parts.len() > 1 { + output.write_char('(').unwrap(); + } + for (i, suffix) in parts.iter().enumerate() { + let mut part = String::new(); + part.write_str(prefix).unwrap(); + if !part.is_empty() { + part.write_str("::").unwrap(); + } + part.write_str(middle).unwrap(); + part.write_str("::").unwrap(); + part.write_str(&suffix.to_upper_camel_case()).unwrap(); + + if i > 0 { + output.write_str(", ").unwrap(); + } + output.write_str(&part).unwrap(); + } + if parts.len() > 1 { + output.write_char(')').unwrap(); + } + + output +} + +fn to_upper_camel_case(i: &Ident) -> Ident { + Ident::new(&i.to_string().to_upper_camel_case(), Span::call_site()) +} + +#[cfg(test)] +mod test { + use super::format_tuple; + + #[test] + fn test_format_tuple() { + assert_eq!(format_tuple("", "Column", "Id"), "Column::Id"); + assert_eq!(format_tuple("super", "Column", "Id"), "super::Column::Id"); + assert_eq!( + format_tuple("", "Column", "(A, B)"), + "(Column::A, Column::B)" + ); + assert_eq!( + format_tuple("super", "Column", "(A, B)"), + "(super::Column::A, super::Column::B)" + ); + } +} diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index 3d3703582b..a16dbfc9a8 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -1,40 +1,54 @@ use heck::ToUpperCamelCase; -use proc_macro2::Span; -use proc_macro2::TokenStream; -use quote::format_ident; -use quote::quote; -use quote::quote_spanned; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; -use syn::Expr; - -use syn::Meta; - -use self::util::GetAsKVMeta; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::{ + Expr, Meta, Type, ext::IdentExt, punctuated::Punctuated, spanned::Spanned, token::Comma, +}; + +use super::from_query_result::{ + DeriveFromQueryResult, FromQueryResultItem, ItemType as FqrItemType, +}; +use super::into_active_model::{DeriveIntoActiveModel, IntoActiveModelField}; +use super::util::GetMeta; #[derive(Debug)] enum Error { InputNotStruct, - EntityNotSpecific, + EntityNotSpecified, NotSupportGeneric(Span), - BothFromColAndFromExpr(Span), + OverlappingAttributes(Span), Syn(syn::Error), } + #[derive(Debug, PartialEq, Eq)] enum ColumnAs { - /// column in the model - Col(syn::Ident), /// alias from a column in model - ColAlias { col: syn::Ident, field: String }, - /// from a expr - Expr { expr: syn::Expr, field_name: String }, + Col { + col: Option, + field: syn::Ident, + }, + /// from an expr + Expr { + expr: syn::Expr, + field: syn::Ident, + }, + /// nesting another struct + Nested { + typ: Type, + field: syn::Ident, + alias: Option, + }, + Skip(syn::Ident), } struct DerivePartialModel { - entity_ident: Option, + entity: Option, + active_model: Option, + model_alias: Option, ident: syn::Ident, fields: Vec, + from_query_result: bool, + into_active_model: bool, } impl DerivePartialModel { @@ -43,31 +57,53 @@ impl DerivePartialModel { return Err(Error::NotSupportGeneric(input.generics.params.span())); } - let syn::Data::Struct(syn::DataStruct{fields:syn::Fields::Named(syn::FieldsNamed{named:fields,..}),..},..) = input.data else{ - return Err(Error::InputNotStruct); + let fields = match input.data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(syn::FieldsNamed { named, .. }), + .. + }) => named, + _ => return Err(Error::InputNotStruct), }; - let mut entity_ident = None; + let mut entity = None; + let mut entity_string = String::new(); + let mut active_model = None; + let mut model_alias = None; + let mut from_query_result = true; + let mut into_active_model = false; for attr in input.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { + if !attr.path().is_ident("sea_orm") { continue; } if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { for meta in list { - entity_ident = meta - .get_as_kv("entity") - .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) - .transpose()?; + if let Some(s) = meta.get_as_kv("entity") { + entity = Some(syn::parse_str::(&s).map_err(Error::Syn)?); + entity_string = s; + } else if let Some(s) = meta.get_as_kv("alias") { + model_alias = Some(s); + } else if let Some(s) = meta.get_as_kv("from_query_result") { + if s == "false" { + from_query_result = false; + } + } else if meta.exists("into_active_model") { + into_active_model = true; + } } } } + if into_active_model { + active_model = Some( + syn::parse_str::(&format!( + "<{entity_string} as EntityTrait>::ActiveModel" + )) + .map_err(Error::Syn)?, + ); + } + let mut column_as_list = Vec::with_capacity(fields.len()); for field in fields { @@ -75,93 +111,241 @@ impl DerivePartialModel { let mut from_col = None; let mut from_expr = None; + let mut nested = false; + let mut nested_alias = None; + let mut skip = false; for attr in field.attrs.iter() { - if !attr.path.is_ident("sea_orm") { + if !attr.path().is_ident("sea_orm") { continue; } if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { for meta in list.iter() { - from_col = meta - .get_as_kv("from_col") - .map(|s| format_ident!("{}", s.to_upper_camel_case())); - from_expr = meta - .get_as_kv("from_expr") - .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) - .transpose()?; + if meta.exists("skip") { + skip = true; + } else if meta.exists("nested") { + nested = true; + } else if let Some(s) = meta.get_as_kv("from_col") { + from_col = Some(format_ident!("{}", s.to_upper_camel_case())); + } else if let Some(s) = meta.get_as_kv("from_expr") { + from_expr = Some(syn::parse_str::(&s).map_err(Error::Syn)?); + } else if let Some(s) = meta.get_as_kv("alias") { + nested_alias = Some(s); + } } } } let field_name = field.ident.unwrap(); - let col_as = match (from_col, from_expr) { - (None, None) => { - if entity_ident.is_none() { - return Err(Error::EntityNotSpecific); + let col_as = match (from_col, from_expr, nested) { + (Some(col), None, false) => { + if entity.is_none() { + return Err(Error::EntityNotSpecified); + } + + ColumnAs::Col { + col: Some(col), + field: field_name, } - ColumnAs::Col(format_ident!( - "{}", - field_name.to_string().to_upper_camel_case() - )) } - (None, Some(expr)) => ColumnAs::Expr { + (None, Some(expr), false) => ColumnAs::Expr { expr, - field_name: field_name.to_string(), + field: field_name, }, - (Some(col), None) => { - if entity_ident.is_none() { - return Err(Error::EntityNotSpecific); + (None, None, true) => ColumnAs::Nested { + typ: field.ty, + field: field_name, + alias: nested_alias, + }, + (None, None, false) => { + if entity.is_none() { + return Err(Error::EntityNotSpecified); + } + if skip { + ColumnAs::Skip(field_name) + } else { + ColumnAs::Col { + col: None, + field: field_name, + } } - - let field = field_name.to_string(); - ColumnAs::ColAlias { col, field } } - (Some(_), Some(_)) => return Err(Error::BothFromColAndFromExpr(field_span)), + (_, _, _) => return Err(Error::OverlappingAttributes(field_span)), }; column_as_list.push(col_as); } Ok(Self { - entity_ident, + entity, + active_model, + model_alias, ident: input.ident, fields: column_as_list, + from_query_result, + into_active_model, }) } fn expand(&self) -> syn::Result { - Ok(self.impl_partial_model_trait()) + let impl_partial_model = self.impl_partial_model(); + + let impl_from_query_result = if self.from_query_result { + DeriveFromQueryResult { + ident: self.ident.clone(), + generics: Default::default(), + fields: self + .fields + .iter() + .map(|col_as| FromQueryResultItem { + typ: match col_as { + ColumnAs::Nested { .. } => FqrItemType::Nested, + ColumnAs::Skip(_) => FqrItemType::Skip, + _ => FqrItemType::Flat, + }, + ident: match col_as { + ColumnAs::Col { field, .. } => field, + ColumnAs::Expr { field, .. } => field, + ColumnAs::Nested { field, .. } => field, + ColumnAs::Skip(field) => field, + } + .to_owned(), + alias: None, + }) + .collect(), + } + .impl_from_query_result(true) + } else { + quote!() + }; + + let impl_into_active_model = if self.into_active_model { + DeriveIntoActiveModel { + ident: self.ident.clone(), + active_model: self.active_model.clone(), + fields: self + .fields + .iter() + .filter_map(|col_as| { + match col_as { + ColumnAs::Col { field, .. } => Some(field), + ColumnAs::Expr { field, .. } => Some(field), + ColumnAs::Nested { .. } => None, + ColumnAs::Skip(_) => None, + } + .map(|f| IntoActiveModelField::Normal(f.clone())) + }) + .collect(), + set_fields: Vec::new(), + exhaustive: false, + } + .impl_into_active_model() + } else { + quote!() + }; + + Ok(quote! { + #impl_partial_model + #impl_from_query_result + #impl_into_active_model + }) } - fn impl_partial_model_trait(&self) -> TokenStream { + fn impl_partial_model(&self) -> TokenStream { let select_ident = format_ident!("select"); let DerivePartialModel { - entity_ident, + entity, + model_alias, ident, fields, + .. } = self; let select_col_code_gen = fields.iter().map(|col_as| match col_as { - ColumnAs::Col(ident) => { - let entity = entity_ident.as_ref().unwrap(); - let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #ident); - quote!(let #select_ident = sea_orm::SelectColumns::select_column(#select_ident, #col_value);) - }, - ColumnAs::ColAlias { col, field } => { - let entity = entity_ident.as_ref().unwrap(); - let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #col); - quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, #field);) - }, - ColumnAs::Expr { expr, field_name } => { - quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name);) - }, + ColumnAs::Col { col, field } => { + let field = field.unraw().to_string(); + let entity = entity.as_ref().unwrap(); + + let variant_name = if let Some(col) = col { + col + } else { + &format_ident!("{}", field.to_upper_camel_case()) + }; + + // variant of the entity column + let column = quote! { + <#entity as sea_orm::EntityTrait>::Column::#variant_name + }; + + // We cast enum as text in select_as if the backend is postgres + + let non_nested = match model_alias { + Some(model_alias) => quote! { + let col_expr = sea_orm::sea_query::Expr::col((#model_alias, #column)); + let casted = sea_orm::ColumnTrait::select_as(&#column, col_expr); + sea_orm::QuerySelect::column_as(#select_ident, casted, col_alias) + }, + None => quote! { + sea_orm::QuerySelect::column_as(#select_ident, #column, col_alias) + }, + }; + + quote! { + let #select_ident = { + let col_alias = pre.map_or(#field.to_string(), |pre| format!("{pre}{}", #field)); + if let Some(nested_alias) = nested_alias { + let alias = sea_orm::sea_query::SeaRc::new(nested_alias); + let col_expr = sea_orm::sea_query::Expr::col( + (alias, #column) + ); + + let casted = sea_orm::ColumnTrait::select_as(&#column, col_expr); + sea_orm::QuerySelect::column_as(#select_ident, casted, col_alias) + } else { + #non_nested + } + }; + } + } + + ColumnAs::Expr { expr, field } => { + let field = field.unraw().to_string(); + quote!(let #select_ident = + if let Some(prefix) = pre { + let ident = format!("{prefix}{}", #field); + sea_orm::QuerySelect::column_as(#select_ident, #expr, ident) + } else { + sea_orm::QuerySelect::column_as(#select_ident, #expr, #field) + }; + ) + } + ColumnAs::Nested { typ, field, alias } => { + let field = field.unraw().to_string(); + let alias_ref: Option<&str> = alias.as_deref(); + let alias_arg = match alias_ref { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }; + quote!(let #select_ident = + <#typ as sea_orm::PartialModelTrait>::select_cols_nested(#select_ident, + Some(&if let Some(prefix) = pre { + format!("{prefix}{}_", #field) + } else { + format!("{}_", #field) + } + ), + #alias_arg + ); + ) + } + ColumnAs::Skip(_) => quote!(), }); quote! { #[automatically_derived] - impl sea_orm::PartialModelTrait for #ident{ - fn select_cols(#select_ident: S) -> S{ + impl sea_orm::PartialModelTrait for #ident { + fn select_cols_nested(#select_ident: S, pre: Option<&str>, nested_alias: Option<&'static str>) -> S { #(#select_col_code_gen)* #select_ident } @@ -176,12 +360,12 @@ pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result partial_model.expand(), Err(Error::NotSupportGeneric(span)) => Ok(quote_spanned! { - span => compile_error!("you can only derive `DerivePartialModel` on named struct"); + span => compile_error!("you can only derive `DerivePartialModel` on concrete struct"); }), - Err(Error::BothFromColAndFromExpr(span)) => Ok(quote_spanned! { - span => compile_error!("you can only use one of `from_col` or `from_expr`"); + Err(Error::OverlappingAttributes(span)) => Ok(quote_spanned! { + span => compile_error!("you can only use one of `from_col`, `from_expr`, `nested`"); }), - Err(Error::EntityNotSpecific) => Ok(quote_spanned! { + Err(Error::EntityNotSpecified) => Ok(quote_spanned! { ident_span => compile_error!("you need specific which entity you are using") }), Err(Error::InputNotStruct) => Ok(quote_spanned! { @@ -191,78 +375,85 @@ pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result Option; - } - - impl GetAsKVMeta for Meta { - fn get_as_kv(&self, k: &str) -> Option { - let Meta::NameValue(MetaNameValue{path, lit:Lit::Str(lit), ..}) = self else { - return None; - }; - - if path.is_ident(k) { - Some(lit.value()) - } else { - None - } - } - } -} - #[cfg(test)] mod test { use quote::format_ident; - use syn::DeriveInput; + use syn::{DeriveInput, Type, parse_str}; use crate::derives::partial_model::ColumnAs; use super::DerivePartialModel; - #[cfg(test)] type StdResult = Result>; - #[cfg(test)] - const CODE_SNIPPET: &str = r#" -#[sea_orm(entity = "Entity")] -struct PartialModel{ - default_field: i32, - #[sea_orm(from_col = "bar")] - alias_field: i32, - #[sea_orm(from_expr = "Expr::val(1).add(1)")] - expr_field : i32 -} -"#; + const CODE_SNIPPET_1: &str = r#" + #[sea_orm(entity = "Entity")] + struct PartialModel { + default_field: i32, + #[sea_orm(from_col = "bar")] + alias_field: i32, + #[sea_orm(from_expr = "Expr::val(1).add(1)")] + expr_field : i32 + } + "#; + #[test] - fn test_load_macro_input() -> StdResult<()> { - let input = syn::parse_str::(CODE_SNIPPET)?; + fn test_load_macro_input_1() -> StdResult<()> { + let input = parse_str::(CODE_SNIPPET_1)?; let middle = DerivePartialModel::new(input).unwrap(); - - assert_eq!(middle.entity_ident, Some(format_ident!("Entity"))); + assert_eq!(middle.entity, Some(parse_str::("Entity").unwrap())); assert_eq!(middle.ident, format_ident!("PartialModel")); assert_eq!(middle.fields.len(), 3); assert_eq!( middle.fields[0], - ColumnAs::Col(format_ident!("DefaultField")) + ColumnAs::Col { + col: None, + field: format_ident!("default_field") + } ); assert_eq!( middle.fields[1], - ColumnAs::ColAlias { - col: format_ident!("Bar"), - field: "alias_field".to_string() + ColumnAs::Col { + col: Some(format_ident!("Bar")), + field: format_ident!("alias_field"), }, ); assert_eq!( middle.fields[2], ColumnAs::Expr { expr: syn::parse_str("Expr::val(1).add(1)").unwrap(), - field_name: "expr_field".to_string() + field: format_ident!("expr_field"), + } + ); + assert_eq!(middle.from_query_result, true); + + Ok(()) + } + + const CODE_SNIPPET_2: &str = r#" + #[sea_orm(entity = "MyEntity", from_query_result = "false")] + struct PartialModel { + default_field: i32, + } + "#; + + #[test] + fn test_load_macro_input_2() -> StdResult<()> { + let input = parse_str::(CODE_SNIPPET_2)?; + + let middle = DerivePartialModel::new(input).unwrap(); + assert_eq!(middle.entity, Some(parse_str::("MyEntity").unwrap())); + assert_eq!(middle.ident, format_ident!("PartialModel")); + assert_eq!(middle.fields.len(), 1); + assert_eq!( + middle.fields[0], + ColumnAs::Col { + col: None, + field: format_ident!("default_field") } ); + assert_eq!(middle.from_query_result, false); Ok(()) } diff --git a/sea-orm-macros/src/derives/primary_key.rs b/sea-orm-macros/src/derives/primary_key.rs index 34e183fb04..6f2048aea5 100644 --- a/sea-orm-macros/src/derives/primary_key.rs +++ b/sea-orm-macros/src/derives/primary_key.rs @@ -1,16 +1,15 @@ -use heck::ToSnakeCase; +use super::impl_iden; use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; -use syn::{punctuated::Punctuated, token::Comma, Data, DataEnum, Fields, Lit, Meta, Variant}; +use syn::{Data, DataEnum, Fields, Variant}; -/// Method to derive a Primary Key for a Model using the [PrimaryKeyTrait](sea_orm::PrimaryKeyTrait) -pub fn expand_derive_primary_key(ident: Ident, data: Data) -> syn::Result { +fn impl_primary_key_to_column(ident: &Ident, data: &Data) -> syn::Result { let variants = match data { syn::Data::Enum(DataEnum { variants, .. }) => variants, _ => { return Ok(quote_spanned! { ident.span() => compile_error!("you can only derive DerivePrimaryKey on enums"); - }) + }); } }; @@ -29,54 +28,7 @@ pub fn expand_derive_primary_key(ident: Ident, data: Data) -> syn::Result = variants - .iter() - .map(|v| { - let mut column_name = v.ident.to_string().to_snake_case(); - for attr in v.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { - if ident != "sea_orm" { - continue; - } - } else { - continue; - } - if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) - { - for meta in list.iter() { - if let Meta::NameValue(nv) = meta { - if let Some(name) = nv.path.get_ident() { - if name == "column_name" { - if let Lit::Str(litstr) = &nv.lit { - column_name = litstr.value(); - } - } - } - } - } - } - } - quote! { #column_name } - }) - .collect(); - Ok(quote!( - #[automatically_derived] - impl sea_orm::Iden for #ident { - fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", sea_orm::IdenStatic::as_str(self)).unwrap(); - } - } - - #[automatically_derived] - impl sea_orm::IdenStatic for #ident { - fn as_str(&self) -> &str { - match self { - #(Self::#variant => #name),* - } - } - } - #[automatically_derived] impl sea_orm::PrimaryKeyToColumn for #ident { type Column = Column; @@ -96,3 +48,15 @@ pub fn expand_derive_primary_key(ident: Ident, data: Data) -> syn::Result syn::Result { + let impl_primary_key_to_column = impl_primary_key_to_column(ident, data)?; + let impl_iden = impl_iden(ident, data)?; + + Ok(quote!( + #impl_primary_key_to_column + + #impl_iden + )) +} diff --git a/sea-orm-macros/src/derives/related_entity.rs b/sea-orm-macros/src/derives/related_entity.rs index bf74a48cc3..558b776b41 100644 --- a/sea-orm-macros/src/derives/related_entity.rs +++ b/sea-orm-macros/src/derives/related_entity.rs @@ -1,53 +1,57 @@ -use heck::ToLowerCamelCase; -use proc_macro2::TokenStream; -use quote::{quote, quote_spanned}; - -use crate::derives::attributes::related_attr; +#[cfg(feature = "seaography")] +mod private { + use heck::ToLowerCamelCase; + use proc_macro_crate::{FoundCrate, crate_name}; + use proc_macro2::{Ident, Span, TokenStream}; + use quote::{quote, quote_spanned}; + + use crate::derives::attributes::related_attr; + + enum Error { + InputNotEnum, + InvalidEntityPath, + Syn(syn::Error), + } -enum Error { - InputNotEnum, - InvalidEntityPath, - Syn(syn::Error), -} + struct DeriveRelatedEntity { + entity_ident: TokenStream, + ident: syn::Ident, + variants: syn::punctuated::Punctuated, + } -struct DeriveRelatedEntity { - entity_ident: TokenStream, - ident: syn::Ident, - variants: syn::punctuated::Punctuated, -} + impl DeriveRelatedEntity { + fn new(input: syn::DeriveInput) -> Result { + let sea_attr = related_attr::SeaOrm::try_from_attributes(&input.attrs) + .map_err(Error::Syn)? + .unwrap_or_default(); + + let ident = input.ident; + let entity_ident = match sea_attr.entity.as_ref().map(Self::parse_lit_string) { + Some(entity_ident) => entity_ident.map_err(|_| Error::InvalidEntityPath)?, + None => quote! { Entity }, + }; + + let variants = match input.data { + syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, + _ => return Err(Error::InputNotEnum), + }; + + Ok(DeriveRelatedEntity { + entity_ident, + ident, + variants, + }) + } -impl DeriveRelatedEntity { - fn new(input: syn::DeriveInput) -> Result { - let sea_attr = related_attr::SeaOrm::try_from_attributes(&input.attrs) - .map_err(Error::Syn)? - .unwrap_or_default(); - - let ident = input.ident; - let entity_ident = match sea_attr.entity.as_ref().map(Self::parse_lit_string) { - Some(entity_ident) => entity_ident.map_err(|_| Error::InvalidEntityPath)?, - None => quote! { Entity }, - }; - - let variants = match input.data { - syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, - _ => return Err(Error::InputNotEnum), - }; - - Ok(DeriveRelatedEntity { - entity_ident, - ident, - variants, - }) - } + fn expand(&self) -> syn::Result { + let ident = &self.ident; + let entity_ident = &self.entity_ident; - fn expand(&self) -> syn::Result { - let ident = &self.ident; - let entity_ident = &self.entity_ident; + let mut get_relation_impl = Vec::new(); + let mut get_relation_name_impl = Vec::new(); + let mut get_related_entity_filter_impl = Vec::new(); - let variant_implementations: Vec = self - .variants - .iter() - .map(|variant| { + for variant in &self.variants { let attr = related_attr::SeaOrm::from_attributes(&variant.attrs)?; let enum_name = &variant.ident; @@ -69,57 +73,99 @@ impl DeriveRelatedEntity { let name = enum_name.to_string().to_lower_camel_case(); - if let Some(def) = def { - Result::<_, syn::Error>::Ok(quote! { - Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) - }) + get_relation_impl.push(if let Some(def) = &def { + quote! { Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) } } else { - Result::<_, syn::Error>::Ok(quote! { - Self::#enum_name => via_builder.get_relation::<#entity_ident, #target_entity>(#name) - }) - } + quote! { Self::#enum_name => via_builder.get_relation::<#entity_ident, #target_entity>(#name) } + }); + get_relation_name_impl.push(if let Some(def) = &def { + quote! { Self::#enum_name => builder.get_relation_name::<#entity_ident, #target_entity>(#name, #def) } + } else { + quote! { Self::#enum_name => via_builder.get_relation_name::<#entity_ident, #target_entity>(#name) } + }); + get_related_entity_filter_impl.push(if let Some(def) = &def { + quote! { Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) } + } else { + quote! { Self::#enum_name => builder.get_relation_via::<#entity_ident, #target_entity>(#name) } + }); + } - }) - .collect::, _>>()?; - - Ok(quote! { - impl seaography::RelationBuilder for #ident { - fn get_relation(&self, context: & 'static seaography::BuilderContext) -> async_graphql::dynamic::Field { - let builder = seaography::EntityObjectRelationBuilder { context }; - let via_builder = seaography::EntityObjectViaRelationBuilder { context }; - match self { - #(#variant_implementations,)* - _ => panic!("No relations for this entity"), + // Get the path of the `async-graphql` on the application's Cargo.toml + let async_graphql_crate = match crate_name("async-graphql") { + // if found, use application's `async-graphql` + Ok(FoundCrate::Name(name)) => { + let ident = Ident::new(&name, Span::call_site()); + quote! { #ident } + } + Ok(FoundCrate::Itself) => quote! { async_graphql }, + // if not, then use the `async-graphql` re-exported by `seaography` + Err(_) => quote! { seaography::async_graphql }, + }; + + Ok(quote! { + impl seaography::RelationBuilder for #ident { + fn get_relation(&self, context: & 'static seaography::BuilderContext) -> #async_graphql_crate::dynamic::Field { + let builder = seaography::EntityObjectRelationBuilder { context }; + let via_builder = seaography::EntityObjectViaRelationBuilder { context }; + match self { + #(#get_relation_impl,)* + _ => panic!("No relations for this entity"), + } + } + fn get_relation_name(&self, context: & 'static seaography::BuilderContext) -> String { + let builder = seaography::EntityObjectRelationBuilder { context }; + let via_builder = seaography::EntityObjectViaRelationBuilder { context }; + match self { + #(#get_relation_name_impl,)* + _ => panic!("No relations for this entity"), + } + } + fn get_related_entity_filter(&self, context: & 'static seaography::BuilderContext) -> seaography::RelatedEntityFilterField { + let builder = seaography::RelatedEntityFilterBuilder { context }; + match self { + #(#get_related_entity_filter_impl,)* + _ => panic!("No relations for this entity"), + } } } + }) + } + fn parse_lit_string(lit: &syn::Lit) -> syn::Result { + match lit { + syn::Lit::Str(lit_str) => lit_str + .value() + .parse() + .map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")), + _ => Err(syn::Error::new_spanned(lit, "attribute must be a string")), } - }) + } } - fn parse_lit_string(lit: &syn::Lit) -> syn::Result { - match lit { - syn::Lit::Str(lit_str) => lit_str - .value() - .parse() - .map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")), - _ => Err(syn::Error::new_spanned(lit, "attribute must be a string")), + /// Method to derive a Related enumeration + pub fn expand_derive_related_entity(input: syn::DeriveInput) -> syn::Result { + let ident_span = input.ident.span(); + + match DeriveRelatedEntity::new(input) { + Ok(model) => model.expand(), + Err(Error::InputNotEnum) => Ok(quote_spanned! { + ident_span => compile_error!("you can only derive DeriveRelation on enums"); + }), + Err(Error::InvalidEntityPath) => Ok(quote_spanned! { + ident_span => compile_error!("invalid attribute value for 'entity'"); + }), + Err(Error::Syn(err)) => Err(err), } } } -/// Method to derive a Related enumeration -pub fn expand_derive_related_entity(input: syn::DeriveInput) -> syn::Result { - let ident_span = input.ident.span(); - - match DeriveRelatedEntity::new(input) { - Ok(model) => model.expand(), - Err(Error::InputNotEnum) => Ok(quote_spanned! { - ident_span => compile_error!("you can only derive DeriveRelation on enums"); - }), - Err(Error::InvalidEntityPath) => Ok(quote_spanned! { - ident_span => compile_error!("invalid attribute value for 'entity'"); - }), - Err(Error::Syn(err)) => Err(err), +#[cfg(not(feature = "seaography"))] +mod private { + use proc_macro2::TokenStream; + + pub fn expand_derive_related_entity(_: syn::DeriveInput) -> syn::Result { + Ok(TokenStream::new()) } } + +pub use private::*; diff --git a/sea-orm-macros/src/derives/relation.rs b/sea-orm-macros/src/derives/relation.rs index 6e47860dbb..f2ed33d38d 100644 --- a/sea-orm-macros/src/derives/relation.rs +++ b/sea-orm-macros/src/derives/relation.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned}; -use super::attributes::{derive_attr, field_attr}; +use super::attributes::{derive_attr, relation_attr}; enum Error { InputNotEnum, @@ -46,18 +46,31 @@ impl DeriveRelation { let entity_ident = &self.entity_ident; let no_relation_def_msg = format!("No RelationDef for {ident}"); + fn lit_str(lit: &syn::Lit) -> syn::Result { + match lit { + syn::Lit::Str(lit_str) => Ok(lit_str.value()), + _ => Err(syn::Error::new_spanned(lit, "attribute must be a string")), + } + } + + fn parse_lit_str(lit: &syn::Lit) -> syn::Result { + lit_str(lit)? + .parse() + .map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")) + } + let variant_relation_defs: Vec = self .variants .iter() .map(|variant| { let variant_ident = &variant.ident; - let attr = field_attr::SeaOrm::from_attributes(&variant.attrs)?; + let attr = relation_attr::SeaOrm::from_attributes(&variant.attrs)?; let mut relation_type = quote! { error }; let related_to = if attr.belongs_to.is_some() { relation_type = quote! { belongs_to }; attr.belongs_to .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'belongs_to'") }) @@ -65,7 +78,7 @@ impl DeriveRelation { relation_type = quote! { has_one }; attr.has_one .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'has_one'") }) @@ -73,7 +86,7 @@ impl DeriveRelation { relation_type = quote! { has_many }; attr.has_many .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'has_many'") }) @@ -84,15 +97,27 @@ impl DeriveRelation { )) }??; - let mut result = quote!( - Self::#variant_ident => #entity_ident::#relation_type(#related_to) - ); + let mut result = if let (Some(has_many), Some(via)) = (&attr.has_many, &attr.via_rel) { + let has_many = lit_str(has_many)?; + let via: TokenStream = if has_many == "Entity" { + lit_str(via)? + } else { + format!("{}::{}", has_many.trim_end_matches("::Entity"), lit_str(via)?) + }.parse().unwrap(); + quote!( + Self::#variant_ident => #entity_ident::has_many_via(#related_to, #via) + ) + } else { + quote!( + Self::#variant_ident => #entity_ident::#relation_type(#related_to) + ) + }; if attr.from.is_some() { let from = attr.from .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'from'") })??; @@ -105,7 +130,7 @@ impl DeriveRelation { let to = attr .to .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'to'") })??; @@ -118,7 +143,7 @@ impl DeriveRelation { let on_update = attr .on_update .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'on_update'") })??; @@ -129,7 +154,7 @@ impl DeriveRelation { let on_delete = attr .on_delete .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'on_delete'") })??; @@ -140,7 +165,7 @@ impl DeriveRelation { let on_condition = attr .on_condition .as_ref() - .map(Self::parse_lit_string) + .map(parse_lit_str) .ok_or_else(|| { syn::Error::new_spanned(variant, "Missing value for 'on_condition'") })??; @@ -163,6 +188,10 @@ impl DeriveRelation { result = quote! { #result.fk_name(#fk_name) }; } + if attr.skip_fk.is_some() { + result = quote! { #result.skip_fk() }; + } + if attr.condition_type.is_some() { let condition_type = attr .condition_type @@ -203,16 +232,6 @@ impl DeriveRelation { } )) } - - fn parse_lit_string(lit: &syn::Lit) -> syn::Result { - match lit { - syn::Lit::Str(lit_str) => lit_str - .value() - .parse() - .map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")), - _ => Err(syn::Error::new_spanned(lit, "attribute must be a string")), - } - } } /// Method to derive a Relation diff --git a/sea-orm-macros/src/derives/try_getable_from_json.rs b/sea-orm-macros/src/derives/try_getable_from_json.rs index 8742d3c9ef..5efdd0e41f 100644 --- a/sea-orm-macros/src/derives/try_getable_from_json.rs +++ b/sea-orm-macros/src/derives/try_getable_from_json.rs @@ -2,6 +2,15 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; pub fn expand_derive_from_json_query_result(ident: Ident) -> syn::Result { + let impl_not_u8 = if cfg!(feature = "postgres-array") { + quote!( + #[automatically_derived] + impl sea_orm::sea_query::postgres_array::NotU8 for #ident {} + ) + } else { + quote!() + }; + Ok(quote!( #[automatically_derived] impl sea_orm::TryGetableFromJson for #ident {} @@ -9,7 +18,12 @@ pub fn expand_derive_from_json_query_result(ident: Ident) -> syn::Result for sea_orm::Value { fn from(source: #ident) -> Self { - sea_orm::Value::Json(serde_json::to_value(&source).ok().map(|s| std::boxed::Box::new(s))) + sea_orm::Value::Json( + Some(std::boxed::Box::new( + serde_json::to_value(&source) + .expect(concat!("Failed to serialize '", stringify!(#ident), "'")) + )) + ) } } @@ -43,5 +57,14 @@ pub fn expand_derive_from_json_query_result(ident: Ident) -> syn::Result for #ident { + fn into_active_value(self) -> sea_orm::ActiveValue<#ident> { + sea_orm::ActiveValue::set(self) + } + } + + #impl_not_u8 )) } diff --git a/sea-orm-macros/src/derives/typed_column.rs b/sea-orm-macros/src/derives/typed_column.rs new file mode 100644 index 0000000000..5aac5e8ec9 --- /dev/null +++ b/sea-orm-macros/src/derives/typed_column.rs @@ -0,0 +1,113 @@ +use super::util::{ + escape_rust_keyword, format_field_ident, is_compound_field, trim_starting_raw_identifier, +}; +use heck::ToUpperCamelCase; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{Data, Expr, Fields, Lit, spanned::Spanned}; + +/// First is `struct TypedColumn`, second is the `const COLUMN` +pub fn expand_typed_column(data: &Data) -> syn::Result<(TokenStream, TokenStream)> { + let mut column_fields = Vec::new(); + let mut column_types = Vec::new(); + let mut column_values = Vec::new(); + + if let Data::Struct(item_struct) = &data { + if let Fields::Named(fields) = &item_struct.fields { + for field in &fields.named { + if let Some(ident) = &field.ident { + let field_name = trim_starting_raw_identifier(ident); + let mut field_name = + Ident::new(&field_name.to_upper_camel_case(), ident.span()); + + let field_type = &field.ty; + let field_type = quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + let mut ignore = false; + let mut column_type = None; + + for attr in field.attrs.iter() { + if attr.path().is_ident("sea_orm") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("column_type") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + column_type = Some(litstr.value()); + } else { + return Err( + meta.error(format!("Invalid column_type {lit:?}")) + ); + } + } else if meta.path.is_ident("enum_name") { + let lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { + let ty: Ident = syn::parse_str(&litstr.value())?; + field_name = ty; + } else { + return Err( + meta.error(format!("Invalid enum_name {lit:?}")) + ); + } + } else if meta.path.is_ident("ignore") { + ignore = true; + } else { + // Reads the value expression to advance the parse stream. + let _: Option = meta.value().and_then(|v| v.parse()).ok(); + } + + Ok(()) + })?; + } + } + + if ignore { + continue; + } + + if is_compound_field(&field_type) { + continue; + } + + field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); + + column_fields.push(format_field_ident(field)); + let wrapper = super::value_type_match::column_type_wrapper( + &column_type, + &field_type, + field.span(), + ); + column_types.push(if let Some(wrapper) = &wrapper { + quote!(sea_orm::#wrapper) + } else { + quote!(Column) + }); + column_values.push(if let Some(wrapper) = &wrapper { + quote!(sea_orm::#wrapper(Column::#field_name)) + } else { + quote!(Column::#field_name) + }); + } + } + } + } + + Ok(( + quote! { + #[doc = " Generated by sea-orm-macros"] + pub struct TypedColumn { + #( + #[doc = " Generated by sea-orm-macros"] + pub #column_fields: #column_types + ),* + } + }, + quote! { + #[doc = " Generated by sea-orm-macros"] + pub const COLUMN: TypedColumn = TypedColumn { + #(#column_fields: #column_values),* + }; + }, + )) +} diff --git a/sea-orm-macros/src/derives/util.rs b/sea-orm-macros/src/derives/util.rs index 7de4b79cd9..276435e67d 100644 --- a/sea-orm-macros/src/derives/util.rs +++ b/sea-orm-macros/src/derives/util.rs @@ -1,10 +1,24 @@ use heck::ToUpperCamelCase; -use quote::format_ident; -use syn::{punctuated::Punctuated, token::Comma, Field, Ident, Meta}; +use syn::{Field, Ident, Meta, MetaNameValue, punctuated::Punctuated, token::Comma}; +/// Remove ignored fields and compound fields pub(crate) fn field_not_ignored(field: &Field) -> bool { + let field_type = &field.ty; + let field_type = quote::quote! { #field_type } + .to_string() // e.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + + if is_compound_field(&field_type) { + return false; + } + + field_not_ignored_compound(field) +} + +/// Remove ignored fields, compound fields okay +pub(crate) fn field_not_ignored_compound(field: &Field) -> bool { for attr in field.attrs.iter() { - if let Some(ident) = attr.path.get_ident() { + if let Some(ident) = attr.path().get_ident() { if ident != "sea_orm" { continue; } @@ -24,11 +38,33 @@ pub(crate) fn field_not_ignored(field: &Field) -> bool { } } } + true } -pub(crate) fn format_field_ident(field: Field) -> Ident { - format_ident!("{}", field.ident.unwrap().to_string()) +pub(crate) fn is_compound_field(field_type: &str) -> bool { + // for #[sea_orm::model] + ((field_type.starts_with("Option<") || field_type.starts_with("Vec<")) && field_type.ends_with("::Entity>")) + // for DeriveModelEx + || field_type.starts_with("HasOne<") || field_type.starts_with("HasMany<") +} + +pub(crate) fn extract_compound_entity(ty: &str) -> &str { + if ty.starts_with("HasMany<") { + &ty["HasMany<".len()..(ty.len() - 1)] + } else if ty.starts_with("HasOne<") { + &ty["HasOne<".len()..(ty.len() - 1)] + } else if ty.starts_with("Option<") { + &ty["Option<".len()..(ty.len() - 1)] + } else if ty.starts_with("Vec<") { + &ty["Vec<".len()..(ty.len() - 1)] + } else { + panic!("Relation applied to non compound type: {ty}") + } +} + +pub(crate) fn format_field_ident(field: &Field) -> Ident { + field.ident.clone().unwrap() } pub(crate) fn trim_starting_raw_identifier(string: T) -> String @@ -74,7 +110,8 @@ where /// - `"A B"` /// - `"A_B"` /// - `"A_ B"` -/// shares the same identifier of `"AB"`; +/// +/// All shares the same identifier of `"AB"`; /// /// This function does the PascelCase conversion with a few special escapes: /// - Non-Unicode Standard Annex #31 compliant characters will converted to their hex notation; @@ -158,6 +195,60 @@ pub(crate) const RUST_KEYWORDS: [&str; 49] = [ pub(crate) const RUST_SPECIAL_KEYWORDS: [&str; 3] = ["crate", "Self", "self"]; +pub(crate) trait GetMeta { + fn exists(&self, k: &str) -> bool; + fn get_as_kv(&self, k: &str) -> Option; + fn get_as_kv_with_ident(&self) -> Option<(Ident, String)>; +} + +impl GetMeta for Meta { + fn exists(&self, key: &str) -> bool { + let Meta::Path(path) = self else { + return false; + }; + path.is_ident(key) + } + + fn get_as_kv(&self, key: &str) -> Option { + let Meta::NameValue(MetaNameValue { + path, + value: syn::Expr::Lit(exprlit), + .. + }) = self + else { + return None; + }; + + let syn::Lit::Str(litstr) = &exprlit.lit else { + return None; + }; + + if path.is_ident(key) { + Some(litstr.value()) + } else { + None + } + } + + fn get_as_kv_with_ident(&self) -> Option<(Ident, String)> { + let Meta::NameValue(MetaNameValue { + path, + value: syn::Expr::Lit(exprlit), + .. + }) = self + else { + return None; + }; + + let syn::Lit::Str(litstr) = &exprlit.lit else { + return None; + }; + + path.get_ident() + .map(|ident| (ident.clone(), litstr.value())) + } +} + #[cfg(test)] mod tests { use super::*; @@ -182,7 +273,7 @@ mod tests { // Test spaced words distinct from non-spaced assert_eq!(camel_case_with_escaped_non_uax31("foo bar"), "Foo0x20bar"); - // Test undescored words distinct from non-spaced and spaced + // Test underscored words distinct from non-spaced and spaced assert_eq!(camel_case_with_escaped_non_uax31("foo_bar"), "Foo0x5Fbar"); // Test leading numeric characters @@ -194,5 +285,11 @@ mod tests { camel_case_with_escaped_non_uax31("1 2 3"), "_0x310x2020x203" ); + + assert_eq!(camel_case_with_escaped_non_uax31("씨오알엠"), "씨오알엠"); + + assert_eq!(camel_case_with_escaped_non_uax31("A_B"), "A0x5Fb"); + + assert_eq!(camel_case_with_escaped_non_uax31("AB"), "Ab"); } } diff --git a/sea-orm-macros/src/derives/value_type.rs b/sea-orm-macros/src/derives/value_type.rs new file mode 100644 index 0000000000..77c8630535 --- /dev/null +++ b/sea-orm-macros/src/derives/value_type.rs @@ -0,0 +1,351 @@ +use super::attributes::value_type_attr; +use super::value_type_match::{array_type_expr, can_try_from_u64, column_type_expr}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Field, Ident, Type, punctuated::Punctuated, spanned::Spanned, token::Comma}; + +#[allow(clippy::large_enum_variant)] +enum DeriveValueType { + TupleStruct(DeriveValueTypeStruct), + StringLike(DeriveValueTypeString), +} + +struct DeriveValueTypeStruct { + name: syn::Ident, + ty: Type, + column_type: TokenStream, + array_type: TokenStream, + can_try_from_u64: bool, +} + +#[derive(Default)] +struct DeriveValueTypeStructAttrs { + column_type: Option, + array_type: Option, + try_from_u64: bool, +} + +impl TryFrom for DeriveValueTypeStructAttrs { + type Error = syn::Error; + + fn try_from(attrs: value_type_attr::SeaOrm) -> syn::Result { + Ok(Self { + column_type: attrs.column_type.map(|s| s.parse()).transpose()?, + array_type: attrs.array_type.map(|s| s.parse()).transpose()?, + try_from_u64: attrs.try_from_u64.is_some(), + }) + } +} + +struct DeriveValueTypeString { + name: syn::Ident, + from_str: Option, + to_str: Option, + column_type: Option, +} + +struct DeriveValueTypeStringAttrs { + from_str: Option, + to_str: Option, + column_type: Option, +} + +impl TryFrom for DeriveValueTypeStringAttrs { + type Error = syn::Error; + + fn try_from(attrs: value_type_attr::SeaOrm) -> syn::Result { + let value_type = attrs.value_type.map(|s| s.value()); + assert_eq!(value_type.as_deref(), Some("String")); + + Ok(Self { + from_str: attrs.from_str.map(|s| s.parse()).transpose()?, + to_str: attrs.to_str.map(|s| s.parse()).transpose()?, + column_type: attrs.column_type.map(|s| s.parse()).transpose()?, + }) + } +} + +impl DeriveValueType { + fn new(input: syn::DeriveInput) -> syn::Result { + // Produce an error if the macro attributes are malformed + let value_type_attr = value_type_attr::SeaOrm::try_from_attributes(&input.attrs)?; + + // If some attributes were set, inspect the optional `value_type` + let value_type = if let Some(ref value_type_attr) = value_type_attr { + value_type_attr.value_type.as_ref().map(|s| s.value()) + } else { + None + }; + + // If either `value_type` is unset, or no attributes were passed, assume + // `DeriveValueTypeStruct`. If no attrs were set, use default values. + if value_type.is_none() || value_type_attr.is_none() { + let value_type_attr = if let Some(value_type_attr) = value_type_attr { + value_type_attr.try_into()? + } else { + DeriveValueTypeStructAttrs::default() + }; + + match input.data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed, .. }), + .. + }) => { + return DeriveValueTypeStruct::new(input.ident, value_type_attr, unnamed) + .map(Self::TupleStruct); + } + _ => { + return Err(syn::Error::new_spanned( + input, + "You can only derive `DeriveValueType` on a struct with a single unnamed field, unless `value_type` is set.", + )); + } + } + } + + let value_type_attr = value_type_attr.unwrap(); + let value_type = value_type.unwrap(); + + match value_type.as_str() { + "String" => DeriveValueTypeString::new(input.ident, value_type_attr.try_into()?) + .map(Self::StringLike), + _ => Err(syn::Error::new_spanned( + input.ident, + r#"Please specify value_type = "String""#, + )), + } + } + + fn expand(&self) -> syn::Result { + Ok(match self { + Self::TupleStruct(s) => s.impl_value_type(), + Self::StringLike(s) => s.impl_value_type(), + }) + } +} + +impl DeriveValueTypeStruct { + fn new( + name: Ident, + attrs: DeriveValueTypeStructAttrs, + fields: Punctuated, + ) -> syn::Result { + let Some(field) = fields.into_iter().next() else { + return Err(syn::Error::new_spanned( + name, + "You can only derive `DeriveValueType` on tuple struct with 1 inner value", + )); + }; + + let field_span = field.span(); + let ty = field.ty; + let field_type = quote! { #ty } + .to_string() //E.g.: "Option < String >" + .replace(' ', ""); // Remove spaces + let field_type = if field_type.starts_with("Option<") { + &field_type[7..(field_type.len() - 1)] // Extract `T` out of `Option` + } else { + field_type.as_str() + }; + + let column_type = column_type_expr(attrs.column_type, field_type, field_span); + let array_type = array_type_expr(attrs.array_type, field_type, field_span); + let can_try_from_u64 = attrs.try_from_u64 || can_try_from_u64(field_type); + + Ok(Self { + name, + ty, + column_type, + array_type, + can_try_from_u64, + }) + } + + fn impl_value_type(&self) -> TokenStream { + let name = &self.name; + let field_type = &self.ty; + let column_type = &self.column_type; + let array_type = &self.array_type; + + let try_from_u64_impl = if self.can_try_from_u64 { + quote!( + #[automatically_derived] + impl sea_orm::TryFromU64 for #name { + fn try_from_u64(n: u64) -> Result { + use std::convert::TryInto; + Ok(Self(n.try_into().map_err(|e| sea_orm::DbErr::TryIntoErr { + from: stringify!(u64), + into: stringify!(#name), + source: std::sync::Arc::new(e), + })?)) + } + } + ) + } else { + quote!() + }; + + let impl_not_u8 = if cfg!(feature = "postgres-array") { + quote!( + #[automatically_derived] + impl sea_orm::sea_query::postgres_array::NotU8 for #name {} + ) + } else { + quote!() + }; + + quote!( + #[automatically_derived] + impl std::convert::From<#name> for sea_orm::Value { + fn from(source: #name) -> Self { + source.0.into() + } + } + + #[automatically_derived] + impl sea_orm::TryGetable for #name { + fn try_get_by(res: &sea_orm::QueryResult, idx: I) + -> std::result::Result { + <#field_type as sea_orm::TryGetable>::try_get_by(res, idx).map(|v| #name(v)) + } + } + + #[automatically_derived] + impl sea_orm::sea_query::ValueType for #name { + fn try_from(v: sea_orm::Value) -> std::result::Result { + <#field_type as sea_orm::sea_query::ValueType>::try_from(v).map(|v| #name(v)) + } + + fn type_name() -> std::string::String { + stringify!(#name).to_owned() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + #array_type + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + #column_type + } + } + + #[automatically_derived] + impl sea_orm::sea_query::Nullable for #name { + fn null() -> sea_orm::Value { + <#field_type as sea_orm::sea_query::Nullable>::null() + } + } + + #[automatically_derived] + impl sea_orm::IntoActiveValue<#name> for #name { + fn into_active_value(self) -> sea_orm::ActiveValue<#name> { + sea_orm::ActiveValue::Set(self) + } + } + + #try_from_u64_impl + + #impl_not_u8 + ) + } +} + +impl DeriveValueTypeString { + fn new(name: Ident, attrs: DeriveValueTypeStringAttrs) -> syn::Result { + Ok(Self { + name, + from_str: attrs.from_str, + to_str: attrs.to_str, + column_type: attrs.column_type, + }) + } + + fn impl_value_type(&self) -> TokenStream { + let name = &self.name; + let from_str = match &self.from_str { + Some(from_str) => from_str, + None => "e!(std::str::FromStr::from_str), + }; + let to_str = match &self.to_str { + Some(to_str) => to_str, + None => "e!(std::string::ToString::to_string), + }; + let column_type = match &self.column_type { + Some(column_type) => column_type, + None => "e!(String(sea_orm::sea_query::StringLen::None)), + }; + + let impl_not_u8 = if cfg!(feature = "postgres-array") { + quote!( + #[automatically_derived] + impl sea_orm::sea_query::postgres_array::NotU8 for #name {} + ) + } else { + quote!() + }; + + quote!( + #[automatically_derived] + impl std::convert::From<#name> for sea_orm::Value { + fn from(source: #name) -> Self { + #to_str(&source).into() + } + } + + #[automatically_derived] + impl sea_orm::TryGetable for #name { + fn try_get_by(res: &sea_orm::QueryResult, idx: I) + -> std::result::Result { + let string = String::try_get_by(res, idx)?; + #from_str(&string).map_err(|err| { + sea_orm::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { + from: "String", + into: stringify!(#name), + source: std::sync::Arc::new(err), + }) + }) + } + } + + #[automatically_derived] + impl sea_orm::sea_query::ValueType for #name { + fn try_from(v: sea_orm::Value) -> std::result::Result { + let string = ::try_from(v)?; + #from_str(&string).map_err(|_| sea_orm::sea_query::ValueTypeErr) + } + + fn type_name() -> std::string::String { + stringify!(#name).to_owned() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::String + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::#column_type + } + } + + #[automatically_derived] + impl sea_orm::sea_query::Nullable for #name { + fn null() -> sea_orm::Value { + sea_orm::Value::String(None) + } + } + + #[automatically_derived] + impl sea_orm::IntoActiveValue<#name> for #name { + fn into_active_value(self) -> sea_orm::ActiveValue<#name> { + sea_orm::ActiveValue::Set(self) + } + } + + #impl_not_u8 + ) + } +} + +pub fn expand_derive_value_type(input: syn::DeriveInput) -> syn::Result { + DeriveValueType::new(input)?.expand() +} diff --git a/sea-orm-macros/src/derives/value_type_match.rs b/sea-orm-macros/src/derives/value_type_match.rs new file mode 100644 index 0000000000..cfba21d052 --- /dev/null +++ b/sea-orm-macros/src/derives/value_type_match.rs @@ -0,0 +1,161 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote_spanned; +use syn::{Ident, LitStr, Type}; + +pub fn column_type_expr( + column_type: Option, + field_type: &str, + field_span: Span, +) -> TokenStream { + match column_type { + Some(column_type) => { + quote_spanned! { field_span => sea_orm::prelude::ColumnType::#column_type } + } + None => { + let ty: Type = LitStr::new(field_type, field_span) + .parse() + .expect("field type error"); + quote_spanned! { field_span => <#ty as sea_orm::sea_query::ValueType>::column_type() } + } + } +} + +pub fn column_type_wrapper( + column_type: &Option, + field_type: &str, + field_span: Span, +) -> Option { + let nullable = trim_option(field_type).0; + + if let Some(column_type) = column_type { + let column_type = if let Some((prefix, _)) = column_type.split_once('(') { + prefix + } else { + column_type + }; + let value_type = match column_type { + "String" | "Text" => { + if nullable { + Some("StringColumnNullable") + } else { + Some("StringColumn") + } + } + "Blob" | "Binary" | "VarBinary" => Some("BytesColumn"), + "TinyInteger" | "SmallInteger" | "Integer" | "BigInteger" | "TinyUnsigned" + | "SmallUnsigned" | "Unsigned" | "BigUnsigned" | "Float" | "Double" | "Decimal" + | "Money" => { + if nullable { + Some("NumericColumnNullable") + } else { + Some("NumericColumn") + } + } + "DateTime" | "Timestamp" | "TimestampWithTimeZone" => Some("DateTimeLikeColumn"), + "Time" => Some("TimeLikeColumn"), + "Date" => Some("DateLikeColumn"), + "Boolean" => Some("BoolColumn"), + "Json" | "JsonBinary" => Some("JsonColumn"), + "Uuid" => Some("UuidColumn"), + "Array" => Some("GenericArrayColumn"), + _ => None, + } + .map(|ty| Ident::new(ty, field_span)); + + if value_type.is_some() { + return value_type; + } + } + + match trim_option(field_type).1 { + "bool" => Some("BoolColumn"), + "String" => { + if nullable { + Some("StringColumnNullable") + } else { + Some("StringColumn") + } + } + "Vec" => Some("BytesColumn"), + "Uuid" => Some("UuidColumn"), + "IpNetwork" => Some("IpNetworkColumn"), + "Json" | "serde_json::Value" => Some("JsonColumn"), + "TextUuid" => Some("TextUuidColumn"), + field_type => { + if is_numeric_column(field_type) || field_type.contains("UnixTimestamp") { + if nullable { + Some("NumericColumnNullable") + } else { + Some("NumericColumn") + } + } else if field_type.starts_with("Vec<") { + let field_type = &field_type["Vec<".len()..field_type.len() - 1]; + if is_numeric_column(field_type) { + Some("NumericArrayColumn") + } else { + Some("GenericArrayColumn") + } + } else if field_type.contains("DateTime") || field_type.contains("Timestamp") { + Some("DateTimeLikeColumn") + } else if field_type.contains("Date") { + Some("DateLikeColumn") + } else if field_type.contains("Time") { + Some("TimeLikeColumn") + } else { + None + } + } + } + .map(|ty| Ident::new(ty, field_span)) +} + +fn is_numeric_column(ty: &str) -> bool { + matches!( + ty, + "i8" | "i16" + | "i32" + | "i64" + | "u8" + | "u16" + | "u32" + | "u64" + | "f32" + | "f64" + | "Decimal" + | "BigDecimal" + ) +} + +pub fn array_type_expr( + array_type: Option, + field_type: &str, + field_span: Span, +) -> TokenStream { + match array_type { + Some(array_type) => { + quote_spanned! { field_span => sea_orm::sea_query::ArrayType::#array_type } + } + None => { + let ty: Type = LitStr::new(field_type, field_span) + .parse() + .expect("field type error"); + quote_spanned! { field_span => <#ty as sea_orm::sea_query::ValueType>::array_type() } + } + } +} + +pub fn can_try_from_u64(field_type: &str) -> bool { + matches!( + field_type, + "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" + ) +} + +/// Return whether it is nullable +fn trim_option(s: &str) -> (bool, &str) { + if s.starts_with("Option<") { + (true, &s["Option<".len()..s.len() - 1]) + } else { + (false, s) + } +} diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 0e9c7b1b8b..0abe34adb9 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, Error}; +use syn::{DeriveInput, Error, parse_macro_input}; #[cfg(feature = "derive")] mod derives; @@ -10,6 +10,8 @@ mod derives; #[cfg(feature = "strum")] mod strum; +mod raw_sql; + /// Create an Entity /// /// ### Usage @@ -21,7 +23,7 @@ mod strum; /// pub struct Entity; /// /// # impl EntityName for Entity { -/// # fn table_name(&self) -> &str { +/// # fn table_name(&self) -> &'static str { /// # "cake" /// # } /// # } @@ -60,7 +62,7 @@ mod strum; /// # fn def(&self) -> ColumnDef { /// # match self { /// # Self::Id => ColumnType::Integer.def(), -/// # Self::Name => ColumnType::String(None).def(), +/// # Self::Name => ColumnType::String(StringLen::None).def(), /// # } /// # } /// # } @@ -134,9 +136,8 @@ pub fn derive_entity(input: TokenStream) -> TokenStream { /// # impl ActiveModelBehavior for ActiveModel {} /// ``` #[cfg(feature = "derive")] -#[proc_macro_derive(DeriveEntityModel, attributes(sea_orm))] +#[proc_macro_derive(DeriveEntityModel, attributes(sea_orm, seaography))] pub fn derive_entity_model(input: TokenStream) -> TokenStream { - let input_ts = input.clone(); let DeriveInput { ident, data, attrs, .. } = parse_macro_input!(input as DeriveInput); @@ -145,16 +146,59 @@ pub fn derive_entity_model(input: TokenStream) -> TokenStream { panic!("Struct name must be Model"); } - let mut ts: TokenStream = derives::expand_derive_entity_model(data, attrs) + let mut ts: TokenStream = derives::expand_derive_entity_model(&data, &attrs) .unwrap_or_else(Error::into_compile_error) .into(); - ts.extend([ - derive_model(input_ts.clone()), - derive_active_model(input_ts), - ]); + + ts.extend::( + derives::expand_derive_model(&ident, &data, &attrs) + .unwrap_or_else(Error::into_compile_error) + .into(), + ); + + ts.extend::( + derives::expand_derive_active_model(&ident, &data) + .unwrap_or_else(Error::into_compile_error) + .into(), + ); + ts } +/// Derive a complex model with relational fields +#[cfg(feature = "derive")] +#[proc_macro_derive(DeriveModelEx, attributes(sea_orm, seaography))] +pub fn derive_model_ex(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, data, attrs, .. + } = parse_macro_input!(input as DeriveInput); + + if ident != "ModelEx" { + panic!("Struct name must be ModelEx"); + } + + derives::expand_derive_model_ex(ident, data, attrs) + .unwrap_or_else(Error::into_compile_error) + .into() +} + +/// Derive a complex active model with relational fields +#[cfg(feature = "derive")] +#[proc_macro_derive(DeriveActiveModelEx, attributes(sea_orm, seaography))] +pub fn derive_active_model_ex(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, data, attrs, .. + } = parse_macro_input!(input as DeriveInput); + + if ident != "ModelEx" { + panic!("Struct name must be ModelEx"); + } + + derives::expand_derive_active_model_ex(&ident, &data, &attrs) + .unwrap_or_else(Error::into_compile_error) + .into() +} + /// The DerivePrimaryKey derive macro will implement [PrimaryKeyToColumn] /// for PrimaryKey which defines tedious mappings between primary keys and columns. /// The [EnumIter] is also derived, allowing iteration over all enum variants. @@ -174,7 +218,7 @@ pub fn derive_entity_model(input: TokenStream) -> TokenStream { /// # pub struct Entity; /// # /// # impl EntityName for Entity { -/// # fn table_name(&self) -> &str { +/// # fn table_name(&self) -> &'static str { /// # "cake" /// # } /// # } @@ -226,7 +270,7 @@ pub fn derive_entity_model(input: TokenStream) -> TokenStream { pub fn derive_primary_key(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input); - match derives::expand_derive_primary_key(ident, data) { + match derives::expand_derive_primary_key(&ident, &data) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } @@ -258,40 +302,6 @@ pub fn derive_column(input: TokenStream) -> TokenStream { } } -/// Derive a column if column names are not in snake-case -/// -/// ### Usage -/// -/// ``` -/// use sea_orm::entity::prelude::*; -/// -/// #[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)] -/// pub enum Column { -/// Id, -/// Name, -/// VendorId, -/// } -/// -/// impl IdenStatic for Column { -/// fn as_str(&self) -> &str { -/// match self { -/// Self::Id => "id", -/// _ => self.default_as_str(), -/// } -/// } -/// } -/// ``` -#[cfg(feature = "derive")] -#[proc_macro_derive(DeriveCustomColumn)] -pub fn derive_custom_column(input: TokenStream) -> TokenStream { - let DeriveInput { ident, data, .. } = parse_macro_input!(input); - - match derives::expand_derive_custom_column(&ident, &data) { - Ok(ts) => ts.into(), - Err(e) => e.to_compile_error().into(), - } -} - /// The DeriveModel derive macro will implement ModelTrait for Model, /// which provides setters and getters for all attributes in the mod /// It also implements FromQueryResult to convert a query result into the corresponding Model. @@ -311,7 +321,7 @@ pub fn derive_custom_column(input: TokenStream) -> TokenStream { /// # pub struct Entity; /// # /// # impl EntityName for Entity { -/// # fn table_name(&self) -> &str { +/// # fn table_name(&self) -> &'static str { /// # "cake" /// # } /// # } @@ -344,7 +354,7 @@ pub fn derive_custom_column(input: TokenStream) -> TokenStream { /// # fn def(&self) -> ColumnDef { /// # match self { /// # Self::Id => ColumnType::Integer.def(), -/// # Self::Name => ColumnType::String(None).def(), +/// # Self::Name => ColumnType::String(StringLen::None).def(), /// # } /// # } /// # } @@ -360,8 +370,11 @@ pub fn derive_custom_column(input: TokenStream) -> TokenStream { #[cfg(feature = "derive")] #[proc_macro_derive(DeriveModel, attributes(sea_orm))] pub fn derive_model(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - derives::expand_derive_model(input) + let DeriveInput { + ident, data, attrs, .. + } = parse_macro_input!(input as DeriveInput); + + derives::expand_derive_model(&ident, &data, &attrs) .unwrap_or_else(Error::into_compile_error) .into() } @@ -384,7 +397,7 @@ pub fn derive_model(input: TokenStream) -> TokenStream { /// # pub struct Entity; /// # /// # impl EntityName for Entity { -/// # fn table_name(&self) -> &str { +/// # fn table_name(&self) -> &'static str { /// # "cake" /// # } /// # } @@ -417,7 +430,7 @@ pub fn derive_model(input: TokenStream) -> TokenStream { /// # fn def(&self) -> ColumnDef { /// # match self { /// # Self::Id => ColumnType::Integer.def(), -/// # Self::Name => ColumnType::String(None).def(), +/// # Self::Name => ColumnType::String(StringLen::None).def(), /// # } /// # } /// # } @@ -435,13 +448,125 @@ pub fn derive_model(input: TokenStream) -> TokenStream { pub fn derive_active_model(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input); - match derives::expand_derive_active_model(ident, data) { + match derives::expand_derive_active_model(&ident, &data) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } } -/// Derive into an active model +/// Will implement [`IntoActiveModel`] for a struct, allowing conversion into an ActiveModel. +/// This is useful for "form" or "input" structs (like from API requests) that turn into +/// an entity's ActiveModel but may omit some fields or provide optional values with defaults. +/// +/// ## Usage: +/// +/// ```rust +/// # mod fruit { +/// # use sea_orm::entity::prelude::*; +/// # #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +/// # #[sea_orm(table_name = "fruit")] +/// # pub struct Model { +/// # #[sea_orm(primary_key)] +/// # pub id: i32, +/// # pub name: String, +/// # pub cake_id: Option, +/// # } +/// # #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +/// # pub enum Relation {} +/// # impl ActiveModelBehavior for ActiveModel {} +/// # } +/// use sea_orm::entity::prelude::*; +/// +/// #[derive(DeriveIntoActiveModel)] +/// #[sea_orm(active_model = "fruit::ActiveModel")] +/// struct NewFruit { +/// name: String, +/// // `id` and `cake_id` are omitted - they become `NotSet` +/// } +/// ``` +/// +/// ## `set(...)` - always set absent ActiveModel fields +/// +/// ```rust +/// # mod fruit { +/// # use sea_orm::entity::prelude::*; +/// # #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +/// # #[sea_orm(table_name = "fruit")] +/// # pub struct Model { +/// # #[sea_orm(primary_key)] +/// # pub id: i32, +/// # pub name: String, +/// # pub cake_id: Option, +/// # } +/// # #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +/// # pub enum Relation {} +/// # impl ActiveModelBehavior for ActiveModel {} +/// # } +/// use sea_orm::entity::prelude::*; +/// +/// #[derive(DeriveIntoActiveModel)] +/// #[sea_orm(active_model = "fruit::ActiveModel", set(cake_id = "None"))] +/// struct NewFruit { +/// name: String, +/// // `cake_id` is not on the struct, but will always be `Set(None)` +/// } +/// ``` +/// +/// ## `default = "expr"` - fallback for `Option` struct fields +/// +/// ```rust +/// # mod fruit { +/// # use sea_orm::entity::prelude::*; +/// # #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +/// # #[sea_orm(table_name = "fruit")] +/// # pub struct Model { +/// # #[sea_orm(primary_key)] +/// # pub id: i32, +/// # pub name: String, +/// # pub cake_id: Option, +/// # } +/// # #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +/// # pub enum Relation {} +/// # impl ActiveModelBehavior for ActiveModel {} +/// # } +/// use sea_orm::entity::prelude::*; +/// +/// #[derive(DeriveIntoActiveModel)] +/// #[sea_orm(active_model = "fruit::ActiveModel")] +/// struct UpdateFruit { +/// /// `Some("Apple")` -> `Set("Apple")`, `None` ->`Set("Unnamed")` +/// #[sea_orm(default = "String::from(\"Unnamed\")")] +/// name: Option, +/// } +/// ``` +/// +/// ## Combining `set(...)`, `default`, `ignore`, and `exhaustive` +/// +/// ```rust +/// # mod fruit { +/// # use sea_orm::entity::prelude::*; +/// # #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +/// # #[sea_orm(table_name = "fruit")] +/// # pub struct Model { +/// # #[sea_orm(primary_key)] +/// # pub id: i32, +/// # pub name: String, +/// # pub cake_id: Option, +/// # } +/// # #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +/// # pub enum Relation {} +/// # impl ActiveModelBehavior for ActiveModel {} +/// # } +/// use sea_orm::entity::prelude::*; +/// +/// #[derive(DeriveIntoActiveModel)] +/// #[sea_orm(active_model = "fruit::ActiveModel", exhaustive, set(cake_id = "None"))] +/// struct NewFruit { +/// id: i32, +/// #[sea_orm(default = "String::from(\"Unnamed\")")] +/// name: Option, +/// } +/// ``` #[cfg(feature = "derive")] #[proc_macro_derive(DeriveIntoActiveModel, attributes(sea_orm))] pub fn derive_into_active_model(input: TokenStream) -> TokenStream { @@ -470,7 +595,7 @@ pub fn derive_into_active_model(input: TokenStream) -> TokenStream { /// # pub struct Entity; /// # /// # impl EntityName for Entity { -/// # fn table_name(&self) -> &str { +/// # fn table_name(&self) -> &'static str { /// # "cake" /// # } /// # } @@ -503,7 +628,7 @@ pub fn derive_into_active_model(input: TokenStream) -> TokenStream { /// # fn def(&self) -> ColumnDef { /// # match self { /// # Self::Id => ColumnType::Integer.def(), -/// # Self::Name => ColumnType::String(None).def(), +/// # Self::Name => ColumnType::String(StringLen::None).def(), /// # } /// # } /// # } @@ -540,11 +665,11 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream { /// - Possible values: `String`, `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64` /// - Note that value has to be passed as string, i.e. `rs_type = "i8"` /// - `db_type`: Define `ColumnType` returned by `ActiveEnum::db_type()` -/// - Possible values: all available enum variants of `ColumnType`, e.g. `String(None)`, `String(Some(1))`, `Integer` +/// - Possible values: all available enum variants of `ColumnType`, e.g. `String(StringLen::None)`, `String(StringLen::N(1))`, `Integer` /// - Note that value has to be passed as string, i.e. `db_type = "Integer"` /// - `enum_name`: Define `String` returned by `ActiveEnum::name()` /// - This attribute is optional with default value being the name of enum in camel-case -/// - Note that value has to be passed as string, i.e. `db_type = "Integer"` +/// - Note that value has to be passed as string, i.e. `enum_name = "MyEnum"` /// /// - For enum variant /// - `string_value` or `num_value`: @@ -558,7 +683,7 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream { /// # Usage /// /// ``` -/// use sea_orm::{entity::prelude::*, DeriveActiveEnum}; +/// use sea_orm::{DeriveActiveEnum, entity::prelude::*}; /// /// #[derive(EnumIter, DeriveActiveEnum)] /// #[sea_orm(rs_type = "i32", db_type = "Integer")] @@ -571,6 +696,7 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream { #[proc_macro_derive(DeriveActiveEnum, attributes(sea_orm))] pub fn derive_active_enum(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); + match derives::expand_derive_active_enum(input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), @@ -579,29 +705,57 @@ pub fn derive_active_enum(input: TokenStream) -> TokenStream { /// Convert a query result into the corresponding Model. /// +/// ### Attributes +/// +/// - `skip`: will not try to pull this field from the query result. And set it to the default value of the type. +/// - `nested`: allows nesting models. can be any type that implements `FromQueryResult` +/// - `alias` / `from_alias`: get the value from this column alias +/// /// ### Usage /// +/// For more complete examples, please refer to https://github.com/SeaQL/sea-orm/blob/master/tests/from_query_result_tests.rs +/// /// ``` -/// use sea_orm::{entity::prelude::*, FromQueryResult}; +/// use sea_orm::{FromQueryResult, entity::prelude::*}; /// -/// #[derive(Debug, FromQueryResult)] -/// struct SelectResult { +/// #[derive(FromQueryResult)] +/// struct Cake { +/// id: i32, /// name: String, -/// num_of_fruits: i32, +/// #[sea_orm(nested)] +/// bakery: Option, +/// #[sea_orm(skip)] +/// skip_me: i32, +/// } +/// +/// #[derive(FromQueryResult)] +/// struct CakeBakery { +/// #[sea_orm(alias = "bakery_id")] +/// id: i32, +/// #[sea_orm(alias = "bakery_name")] +/// title: String, +/// } +/// ``` +/// +/// You can compose this with regular Models, if there's no column collision: +/// +/// ```ignore +/// #[derive(FromQueryResult)] +/// struct CakePlain { +/// id: i32, +/// name: String, +/// price: Decimal, +/// #[sea_orm(nested)] +/// baker: Option, /// } /// ``` #[cfg(feature = "derive")] -#[proc_macro_derive(FromQueryResult)] +#[proc_macro_derive(FromQueryResult, attributes(sea_orm))] pub fn derive_from_query_result(input: TokenStream) -> TokenStream { - let DeriveInput { - ident, - data, - generics, - .. - } = parse_macro_input!(input); + let derive_input = parse_macro_input!(input); - match derives::expand_derive_from_query_result(ident, data, generics) { - Ok(ts) => ts.into(), + match derives::expand_derive_from_query_result(derive_input) { + Ok(token_stream) => token_stream.into(), Err(e) => e.to_compile_error().into(), } } @@ -670,13 +824,9 @@ pub fn derive_relation(input: TokenStream) -> TokenStream { #[proc_macro_derive(DeriveRelatedEntity, attributes(sea_orm))] pub fn derive_related_entity(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - if cfg!(feature = "seaography") { - derives::expand_derive_related_entity(input) - .unwrap_or_else(Error::into_compile_error) - .into() - } else { - TokenStream::new() - } + derives::expand_derive_related_entity(input) + .unwrap_or_else(Error::into_compile_error) + .into() } /// The DeriveMigrationName derive macro will implement `sea_orm_migration::MigrationName` for a migration. @@ -718,15 +868,18 @@ pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { } } -/// The DerivePartialModel derive macro will implement `sea_orm::PartialModelTrait` for simplify partial model queries. +/// The DerivePartialModel derive macro will implement [`sea_orm::PartialModelTrait`] for simplify partial model queries. +/// Since 2.0, this macro cannot be used with the `FromQueryResult` macro. /// /// ## Usage /// +/// For more complete examples, please refer to https://github.com/SeaQL/sea-orm/blob/master/tests/partial_model_tests.rs +/// /// ```rust -/// use sea_orm::{entity::prelude::*, sea_query::Expr, DerivePartialModel, FromQueryResult}; -/// use serde::{Deserialize, Serialize}; +/// use sea_orm::sea_query::ExprTrait; +/// use sea_orm::{DerivePartialModel, entity::prelude::*}; /// -/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] +/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] /// #[sea_orm(table_name = "posts")] /// pub struct Model { /// #[sea_orm(primary_key)] @@ -735,18 +888,11 @@ pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { /// #[sea_orm(column_type = "Text")] /// pub text: String, /// } -/// # #[derive(Copy, Clone, Debug, EnumIter)] +/// # #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] /// # pub enum Relation {} -/// # -/// # impl RelationTrait for Relation { -/// # fn def(&self) -> RelationDef { -/// # panic!("No Relation"); -/// # } -/// # } -/// # /// # impl ActiveModelBehavior for ActiveModel {} /// -/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// #[derive(Debug, DerivePartialModel)] /// #[sea_orm(entity = "Entity")] /// struct SelectResult { /// title: String, @@ -757,28 +903,128 @@ pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// If all fields in the partial model is `from_expr`, the `entity` can be ignore. +/// If all fields in the partial model is `from_expr`, the specifying the `entity` can be skipped. /// ``` -/// use sea_orm::{entity::prelude::*, sea_query::Expr, DerivePartialModel, FromQueryResult}; +/// use sea_orm::{ +/// DerivePartialModel, +/// entity::prelude::*, +/// sea_query::{Expr, ExprTrait}, +/// }; /// -/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// #[derive(Debug, DerivePartialModel)] /// struct SelectResult { /// #[sea_orm(from_expr = "Expr::val(1).add(1)")] /// sum: i32, /// } /// ``` /// -/// A field cannot have attributes `from_col` and `from_expr` at the same time. +/// Since SeaORM 1.1.7, `DerivePartialModel` can also derive `FromQueryResult`. +/// This is necessary to support nested partial models. +/// Since 2.0, `from_query_result` is implemented by default, unless `from_query_result = "false"`. +/// +/// ``` +/// use sea_orm::DerivePartialModel; +/// # +/// # mod cake { +/// # use sea_orm::entity::prelude::*; +/// # #[derive(Clone, Debug, DeriveEntityModel)] +/// # #[sea_orm(table_name = "cake")] +/// # pub struct Model { +/// # #[sea_orm(primary_key)] +/// # pub id: i32, +/// # pub name: String, +/// # } +/// # #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] +/// # pub enum Relation {} +/// # impl ActiveModelBehavior for ActiveModel {} +/// # } +/// # +/// # mod bakery { +/// # use sea_orm::entity::prelude::*; +/// # #[derive(Clone, Debug, DeriveEntityModel)] +/// # #[sea_orm(table_name = "bakery")] +/// # pub struct Model { +/// # #[sea_orm(primary_key)] +/// # pub id: i32, +/// # pub name: String, +/// # } +/// # #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] +/// # pub enum Relation {} +/// # impl ActiveModelBehavior for ActiveModel {} +/// # } +/// +/// #[derive(DerivePartialModel)] +/// #[sea_orm(entity = "cake::Entity")] +/// struct Cake { +/// id: i32, +/// name: String, +/// #[sea_orm(nested)] +/// bakery: Option, +/// #[sea_orm(skip)] +/// ignore: String, +/// } +/// +/// #[derive(DerivePartialModel)] +/// #[sea_orm(entity = "bakery::Entity")] +/// struct Bakery { +/// id: i32, +/// #[sea_orm(from_col = "Name")] +/// title: String, +/// } +/// +/// // In addition, there's an `alias` attribute to select the columns from an alias: +/// +/// #[derive(DerivePartialModel)] +/// #[sea_orm(entity = "bakery::Entity", alias = "factory")] +/// struct Factory { +/// id: i32, +/// #[sea_orm(from_col = "name")] +/// plant: String, +/// } +/// +/// #[derive(DerivePartialModel)] +/// #[sea_orm(entity = "cake::Entity")] +/// struct CakeFactory { +/// id: i32, +/// name: String, +/// #[sea_orm(nested)] +/// bakery: Option, +/// } +/// ``` +/// +/// ```ignore +/// let cake: CakeFactory = cake::Entity::find() +/// .join_as( +/// JoinType::LeftJoin, +/// cake::Relation::Bakery.def(), +/// "factory", +/// ) +/// .order_by_asc(cake::Column::Id) +/// .into_partial_model() +/// .one(&db) +/// .await +/// .unwrap() +/// .unwrap() +/// +/// SELECT +/// "cake"."id" AS "id", "cake"."name" AS "name", +/// "factory"."id" AS "bakery_id", "factory"."name" AS "bakery_plant" +/// FROM "cake" +/// LEFT JOIN "bakery" AS "factory" ON "cake"."bakery_id" = "factory"."id" +/// LIMIT 1 +/// ``` +/// +/// A field cannot have attributes `from_col`, `from_expr` or `nested` at the same time. /// Or, it will result in a compile error. /// /// ```compile_fail -/// use sea_orm::{entity::prelude::*, FromQueryResult, DerivePartialModel, sea_query::Expr}; +/// use sea_orm::{entity::prelude::*, DerivePartialModel, sea_query::Expr}; /// -/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// #[derive(Debug, DerivePartialModel)] /// #[sea_orm(entity = "Entity")] /// struct SelectResult { /// #[sea_orm(from_expr = "Expr::val(1).add(1)", from_col = "foo")] -/// sum: i32 +/// sum: i32, /// } /// ``` #[cfg(feature = "derive")] @@ -803,18 +1049,41 @@ pub fn test(_: TokenStream, input: TokenStream) -> TokenStream { let body = &input.block; let attrs = &input.attrs; - quote::quote! ( - #[test] - #(#attrs)* - fn #name() #ret { - let _ = ::tracing_subscriber::fmt() - .with_max_level(::tracing::Level::DEBUG) - .with_test_writer() - .try_init(); - crate::block_on!(async { #body }) - } - ) - .into() + if cfg!(feature = "async") { + quote::quote! ( + #[test] + #[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres", + ))] + #(#attrs)* + fn #name() #ret { + let _ = ::tracing_subscriber::fmt() + .with_max_level(::tracing::Level::DEBUG) + .with_test_writer() + .try_init(); + crate::block_on!(async { #body }) + } + ) + .into() + } else { + quote::quote! ( + #[test] + #[cfg(any( + feature = "rusqlite", + ))] + #(#attrs)* + fn #name() #ret { + let _ = ::tracing_subscriber::fmt() + .with_max_level(::tracing::Level::DEBUG) + .with_test_writer() + .try_init(); + #body + } + ) + .into() + } } /// Creates a new type that iterates of the variants of an enum. @@ -832,3 +1101,224 @@ pub fn enum_iter(input: TokenStream) -> TokenStream { .unwrap_or_else(Error::into_compile_error) .into() } + +/// Implements traits for types that wrap a database value type. +/// +/// This procedure macro implements `From for Value`, `sea_orm::TryGetTable`, and +/// `sea_query::ValueType` for the wrapper type `T`. +/// +/// It also implements `sea_query::postgres_array::NotU8`. +/// It implements `TryFromU64` if the wrapped types are recognized primitives, i.e. +/// `"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64"`. +/// +/// The wrapped type must be `sea_orm::Value` compatible. +/// +/// ## Usage +/// +/// ```rust +/// use sea_orm::DeriveValueType; +/// +/// #[derive(DeriveValueType)] +/// struct MyString(String); +/// +/// #[derive(DeriveValueType)] +/// struct MyNumber(i32); +/// ``` +/// +/// It's also possible to derive value type for enum-strings. +/// Basically the underlying type is String, and the custom must implement methods `to_str` and `from_str`. +/// +/// ## Example +/// +/// ```rust +/// use sea_orm::{DeriveValueType, sea_query::ValueTypeErr}; +/// +/// #[derive(DeriveValueType)] +/// #[sea_orm(value_type = "String")] +/// pub enum Tag { +/// Hard, +/// Soft, +/// } +/// +/// impl std::fmt::Display for Tag { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// write!( +/// f, +/// "{}", +/// match self { +/// Self::Hard => "hard", +/// Self::Soft => "soft", +/// } +/// ) +/// } +/// } +/// +/// impl std::str::FromStr for Tag { +/// type Err = ValueTypeErr; +/// +/// fn from_str(s: &str) -> Result { +/// Ok(match s { +/// "hard" => Self::Hard, +/// "soft" => Self::Soft, +/// _ => return Err(ValueTypeErr), +/// }) +/// } +/// } +/// ``` +/// +/// `from_str` defaults to `std::str::FromStr::from_str`. `to_str` defaults to `std::string::ToString::to_string`. +/// They can be overridden with custom functions. +/// +/// ```rust +/// use sea_orm::{DeriveValueType, sea_query::ValueTypeErr}; +/// +/// #[derive(DeriveValueType)] +/// #[sea_orm( +/// value_type = "String", +/// from_str = "Tag::from_str", +/// to_str = "Tag::to_str" +/// )] +/// pub enum Tag { +/// Color, +/// Grey, +/// } +/// +/// impl Tag { +/// fn to_str(&self) -> &'static str { +/// match self { +/// Self::Color => "color", +/// Self::Grey => "grey", +/// } +/// } +/// +/// fn from_str(s: &str) -> Result { +/// Ok(match s { +/// "color" => Self::Color, +/// "grey" => Self::Grey, +/// _ => return Err(ValueTypeErr), +/// }) +/// } +/// } +/// ``` +#[cfg(feature = "derive")] +#[proc_macro_derive(DeriveValueType, attributes(sea_orm))] +pub fn derive_value_type(input: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(input as DeriveInput); + match derives::expand_derive_value_type(derive_input) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[cfg(feature = "derive")] +#[proc_macro_derive(DeriveDisplay, attributes(sea_orm))] +pub fn derive_active_enum_display(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match derives::expand_derive_active_enum_display(input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + +/// The DeriveIden derive macro will implement `sea_orm::Iden` for simplify Iden implementation. +/// +/// ## Usage +/// +/// ```rust +/// use sea_orm::{DeriveIden, Iden}; +/// +/// #[derive(DeriveIden)] +/// pub enum MyClass { +/// Table, // this is a special case, which maps to the enum's name +/// Id, +/// #[sea_orm(iden = "turtle")] +/// Title, +/// Text, +/// } +/// +/// #[derive(DeriveIden)] +/// struct MyOther; +/// +/// assert_eq!(MyClass::Table.to_string(), "my_class"); +/// assert_eq!(MyClass::Id.to_string(), "id"); +/// assert_eq!(MyClass::Title.to_string(), "turtle"); // renamed! +/// assert_eq!(MyClass::Text.to_string(), "text"); +/// assert_eq!(MyOther.to_string(), "my_other"); +/// ``` +#[cfg(feature = "derive")] +#[proc_macro_derive(DeriveIden, attributes(sea_orm))] +pub fn derive_iden(input: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(input as DeriveInput); + + match derives::expand_derive_iden(derive_input) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + +/// The DeriveArrowSchema derive macro generates an `arrow_schema()` method +/// that returns an Apache Arrow schema from a SeaORM entity definition. +/// +/// This macro is automatically applied when `#[sea_orm(arrow_schema)]` is specified +/// on a model. You typically don't need to use this derive macro directly. +/// +/// ## Usage +/// +/// ```rust,ignore +/// use sea_orm::entity::prelude::*; +/// +/// #[sea_orm::model] +/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +/// #[sea_orm(table_name = "user", arrow_schema)] +/// pub struct Model { +/// #[sea_orm(primary_key)] +/// pub id: i64, +/// pub name: String, +/// #[sea_orm(column_type = "Decimal(Some((20, 4)))")] +/// pub amount: Decimal, +/// } +/// ``` +#[cfg(feature = "derive")] +#[proc_macro_derive(DeriveArrowSchema, attributes(sea_orm))] +pub fn derive_arrow_schema(input: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(input as DeriveInput); + + match derives::expand_derive_arrow_schema( + derive_input.ident, + derive_input.data, + derive_input.attrs, + ) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[proc_macro] +pub fn raw_sql(input: TokenStream) -> TokenStream { + match raw_sql::expand(input) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[cfg(feature = "derive")] +#[proc_macro_attribute] +pub fn sea_orm_model(_attr: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::ItemStruct); + + match derives::expand_sea_orm_model(input, false) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[cfg(feature = "derive")] +#[proc_macro_attribute] +pub fn sea_orm_compact_model(_attr: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::ItemStruct); + + match derives::expand_sea_orm_model(input, true) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} diff --git a/sea-orm-macros/src/raw_sql.rs b/sea-orm-macros/src/raw_sql.rs new file mode 100644 index 0000000000..5a77622873 --- /dev/null +++ b/sea-orm-macros/src/raw_sql.rs @@ -0,0 +1,49 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + Ident, LitStr, Token, + parse::{Parse, ParseStream}, +}; + +struct CallArgs { + backend: Ident, + _comma: Token![,], + sql_string: LitStr, +} + +impl Parse for CallArgs { + fn parse(input: ParseStream) -> syn::Result { + Ok(CallArgs { + backend: input.parse()?, + _comma: input.parse()?, + sql_string: input.parse()?, + }) + } +} + +pub fn expand(input: proc_macro::TokenStream) -> syn::Result { + let CallArgs { + backend, + sql_string, + .. + } = syn::parse(input)?; + + let builder = match backend.to_string().as_str() { + "MySql" => quote!(MysqlQueryBuilder), + "Postgres" => quote!(PostgresQueryBuilder), + "Sqlite" => quote!(SqliteQueryBuilder), + backend => panic!("Unsupported backend {backend}"), + }; + + Ok(quote! {{ + use sea_orm::sea_query; + + let query = sea_query::raw_query!(#builder, #sql_string); + + sea_orm::Statement { + sql: query.sql, + values: Some(query.values), + db_backend: sea_orm::DbBackend::#backend, + } + }}) +} diff --git a/sea-orm-macros/src/strum/enum_iter.rs b/sea-orm-macros/src/strum/enum_iter.rs index e4f153ad5e..e26aebf4ef 100644 --- a/sea-orm-macros/src/strum/enum_iter.rs +++ b/sea-orm-macros/src/strum/enum_iter.rs @@ -2,18 +2,18 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{Data, DeriveInput, Fields, Ident}; -use super::helpers::{non_enum_error, HasStrumVariantProperties, HasTypeProperties}; +use super::helpers::{HasStrumVariantProperties, HasTypeProperties, non_enum_error}; pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; - let gen = &ast.generics; - let (impl_generics, ty_generics, where_clause) = gen.split_for_impl(); + let r#gen = &ast.generics; + let (impl_generics, ty_generics, where_clause) = r#gen.split_for_impl(); let vis = &ast.vis; let type_properties = ast.get_type_properties()?; let strum_module_path = type_properties.crate_module_path(); - let doc_comment = format!("An iterator over the variants of [{}]", name); + let doc_comment = format!("An iterator over the variants of [{name}]"); - if gen.lifetimes().count() > 0 { + if r#gen.lifetimes().count() > 0 { return Err(syn::Error::new( Span::call_site(), "This macro doesn't support enums with lifetimes. \ @@ -21,8 +21,8 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { )); } - let phantom_data = if gen.type_params().count() > 0 { - let g = gen.type_params().map(|param| ¶m.ident); + let phantom_data = if r#gen.type_params().count() > 0 { + let g = r#gen.type_params().map(|param| ¶m.ident); quote! { < ( #(#g),* ) > } } else { quote! { < () > } @@ -44,8 +44,10 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { let params = match &variant.fields { Fields::Unit => quote! {}, Fields::Unnamed(fields) => { - let defaults = ::core::iter::repeat(quote!(::core::default::Default::default())) - .take(fields.unnamed.len()); + let defaults = ::core::iter::repeat_n( + quote!(::core::default::Default::default()), + fields.unnamed.len(), + ); quote! { (#(#defaults),*) } } Fields::Named(fields) => { @@ -63,25 +65,25 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { let variant_count = arms.len(); arms.push(quote! { _ => ::core::option::Option::None }); - let iter_name = syn::parse_str::(&format!("{}Iter", name)).unwrap(); + let iter_name = syn::parse_str::(&format!("{name}Iter")).unwrap(); // Create a string literal "MyEnumIter" to use in the debug impl. let iter_name_debug_struct = - syn::parse_str::(&format!("\"{}\"", iter_name)).unwrap(); + syn::parse_str::(&format!("\"{iter_name}\"")).unwrap(); Ok(quote! { #[doc = #doc_comment] #[allow( missing_copy_implementations, )] - #vis struct #iter_name #ty_generics { + #vis struct #iter_name #impl_generics { idx: usize, back_idx: usize, marker: ::core::marker::PhantomData #phantom_data, } - impl #impl_generics core::fmt::Debug for #iter_name #ty_generics #where_clause { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + impl #impl_generics ::core::fmt::Debug for #iter_name #ty_generics #where_clause { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { // We don't know if the variants implement debug themselves so the only thing we // can really show is how many elements are left. f.debug_struct(#iter_name_debug_struct) @@ -91,7 +93,7 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { } impl #impl_generics #iter_name #ty_generics #where_clause { - fn get(&self, idx: usize) -> Option<#name #ty_generics> { + fn get(&self, idx: usize) -> ::core::option::Option<#name #ty_generics> { match idx { #(#arms),* } @@ -112,16 +114,16 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { impl #impl_generics Iterator for #iter_name #ty_generics #where_clause { type Item = #name #ty_generics; - fn next(&mut self) -> Option<::Item> { + fn next(&mut self) -> ::core::option::Option<::Item> { self.nth(0) } - fn size_hint(&self) -> (usize, Option) { + fn size_hint(&self) -> (usize, ::core::option::Option) { let t = if self.idx + self.back_idx >= #variant_count { 0 } else { #variant_count - self.idx - self.back_idx }; (t, Some(t)) } - fn nth(&mut self, n: usize) -> Option<::Item> { + fn nth(&mut self, n: usize) -> ::core::option::Option<::Item> { let idx = self.idx + n + 1; if idx + self.back_idx > #variant_count { // We went past the end of the iterator. Freeze idx at #variant_count @@ -131,7 +133,7 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { ::core::option::Option::None } else { self.idx = idx; - self.get(idx - 1) + #iter_name::get(self, idx - 1) } } } @@ -143,7 +145,7 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { } impl #impl_generics DoubleEndedIterator for #iter_name #ty_generics #where_clause { - fn next_back(&mut self) -> Option<::Item> { + fn next_back(&mut self) -> ::core::option::Option<::Item> { let back_idx = self.back_idx + 1; if self.idx + back_idx > #variant_count { @@ -154,11 +156,13 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { ::core::option::Option::None } else { self.back_idx = back_idx; - self.get(#variant_count - self.back_idx) + #iter_name::get(self, #variant_count - self.back_idx) } } } + impl #impl_generics ::core::iter::FusedIterator for #iter_name #ty_generics #where_clause { } + impl #impl_generics Clone for #iter_name #ty_generics #where_clause { fn clone(&self) -> #iter_name #ty_generics { #iter_name { diff --git a/sea-orm-macros/src/strum/helpers/case_style.rs b/sea-orm-macros/src/strum/helpers/case_style.rs index 425382606e..45eca92f6f 100644 --- a/sea-orm-macros/src/strum/helpers/case_style.rs +++ b/sea-orm-macros/src/strum/helpers/case_style.rs @@ -3,8 +3,8 @@ use heck::{ }; use std::str::FromStr; use syn::{ - parse::{Parse, ParseStream}, Ident, LitStr, + parse::{Parse, ParseStream}, }; #[allow(clippy::enum_variant_names)] @@ -44,8 +44,7 @@ impl Parse for CaseStyle { syn::Error::new_spanned( &text, format!( - "Unexpected case style for serialize_all: `{}`. Valid values are: `{:?}`", - val, VALID_CASE_STYLES + "Unexpected case style for serialize_all: `{val}`. Valid values are: `{VALID_CASE_STYLES:?}`", ), ) }) diff --git a/sea-orm-macros/src/strum/helpers/metadata.rs b/sea-orm-macros/src/strum/helpers/metadata.rs index 56e4c78bd8..5a30fbbecf 100644 --- a/sea-orm-macros/src/strum/helpers/metadata.rs +++ b/sea-orm-macros/src/strum/helpers/metadata.rs @@ -1,12 +1,10 @@ -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use syn::{ - parenthesized, + Attribute, DeriveInput, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue, Path, + Token, Variant, Visibility, parenthesized, parse::{Parse, ParseStream}, - parse2, parse_str, + parse_str, parse2, punctuated::Punctuated, - spanned::Spanned, - Attribute, DeriveInput, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue, Path, Token, Variant, - Visibility, }; use super::case_style::CaseStyle; @@ -76,17 +74,6 @@ impl Parse for EnumMeta { } } -impl Spanned for EnumMeta { - fn span(&self) -> Span { - match self { - EnumMeta::SerializeAll { kw, .. } => kw.span(), - EnumMeta::AsciiCaseInsensitive(kw) => kw.span(), - EnumMeta::Crate { kw, .. } => kw.span(), - EnumMeta::UsePhf(use_phf) => use_phf.span(), - } - } -} - pub enum EnumDiscriminantsMeta { Derive { kw: kw::derive, paths: Vec }, Name { kw: kw::name, name: Ident }, @@ -100,7 +87,7 @@ impl Parse for EnumDiscriminantsMeta { let kw = input.parse()?; let content; parenthesized!(content in input); - let paths = content.parse_terminated::<_, Token![,]>(Path::parse)?; + let paths = content.parse_terminated(Path::parse, Token![,])?; Ok(EnumDiscriminantsMeta::Derive { kw, paths: paths.into_iter().collect(), @@ -127,17 +114,6 @@ impl Parse for EnumDiscriminantsMeta { } } -impl Spanned for EnumDiscriminantsMeta { - fn span(&self) -> Span { - match self { - EnumDiscriminantsMeta::Derive { kw, .. } => kw.span, - EnumDiscriminantsMeta::Name { kw, .. } => kw.span, - EnumDiscriminantsMeta::Vis { kw, .. } => kw.span, - EnumDiscriminantsMeta::Other { path, .. } => path.span(), - } - } -} - pub trait DeriveInputExt { /// Get all the strum metadata associated with an enum. fn get_metadata(&self) -> syn::Result>; @@ -228,7 +204,7 @@ impl Parse for VariantMeta { let kw = input.parse()?; let content; parenthesized!(content in input); - let props = content.parse_terminated::<_, Token![,]>(Prop::parse)?; + let props = content.parse_terminated(Prop::parse, Token![,])?; Ok(VariantMeta::Props { kw, props: props @@ -256,22 +232,6 @@ impl Parse for Prop { } } -impl Spanned for VariantMeta { - fn span(&self) -> Span { - match self { - VariantMeta::Message { kw, .. } => kw.span, - VariantMeta::DetailedMessage { kw, .. } => kw.span, - VariantMeta::Documentation { value } => value.span(), - VariantMeta::Serialize { kw, .. } => kw.span, - VariantMeta::ToString { kw, .. } => kw.span, - VariantMeta::Disabled(kw) => kw.span, - VariantMeta::Default(kw) => kw.span, - VariantMeta::AsciiCaseInsensitive { kw, .. } => kw.span, - VariantMeta::Props { kw, .. } => kw.span, - } - } -} - pub trait VariantExt { /// Get all the metadata associated with an enum variant. fn get_metadata(&self) -> syn::Result>; @@ -282,26 +242,32 @@ impl VariantExt for Variant { let result = get_metadata_inner("strum", &self.attrs)?; self.attrs .iter() - .filter(|attr| attr.path.is_ident("doc")) + .filter(|attr| attr.path().is_ident("doc")) .try_fold(result, |mut vec, attr| { if let Meta::NameValue(MetaNameValue { - lit: Lit::Str(value), + value: + Expr::Lit(ExprLit { + lit: Lit::Str(value), + .. + }), .. - }) = attr.parse_meta()? + }) = &attr.meta { - vec.push(VariantMeta::Documentation { value }) + vec.push(VariantMeta::Documentation { + value: value.clone(), + }) } Ok(vec) }) } } -fn get_metadata_inner<'a, T: Parse + Spanned>( +fn get_metadata_inner<'a, T: Parse>( ident: &str, it: impl IntoIterator, ) -> syn::Result> { it.into_iter() - .filter(|attr| attr.path.is_ident(ident)) + .filter(|attr| attr.path().is_ident(ident)) .try_fold(Vec::new(), |mut vec, attr| { vec.extend(attr.parse_args_with(Punctuated::::parse_terminated)?); Ok(vec) diff --git a/sea-orm-macros/src/strum/helpers/mod.rs b/sea-orm-macros/src/strum/helpers/mod.rs index 77049eb4b6..1165cc9ee2 100644 --- a/sea-orm-macros/src/strum/helpers/mod.rs +++ b/sea-orm-macros/src/strum/helpers/mod.rs @@ -1,4 +1,3 @@ -pub use self::case_style::CaseStyleHelpers; pub use self::type_props::HasTypeProperties; pub use self::variant_props::HasStrumVariantProperties; @@ -15,10 +14,8 @@ pub fn non_enum_error() -> syn::Error { } pub fn occurrence_error(fst: T, snd: T, attr: &str) -> syn::Error { - let mut e = syn::Error::new_spanned( - snd, - format!("Found multiple occurrences of strum({})", attr), - ); + let mut e = + syn::Error::new_spanned(snd, format!("Found multiple occurrences of strum({attr})")); e.combine(syn::Error::new_spanned(fst, "first one here")); e } diff --git a/sea-orm-macros/src/strum/helpers/type_props.rs b/sea-orm-macros/src/strum/helpers/type_props.rs index 3b127eb3cf..cd49602bd4 100644 --- a/sea-orm-macros/src/strum/helpers/type_props.rs +++ b/sea-orm-macros/src/strum/helpers/type_props.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use quote::quote; use std::default::Default; -use syn::{parse_quote, DeriveInput, Ident, Path, Visibility}; +use syn::{DeriveInput, Ident, Path, Visibility, parse_quote}; use super::case_style::CaseStyle; use super::metadata::{DeriveInputExt, EnumDiscriminantsMeta, EnumMeta}; diff --git a/sea-orm-macros/src/strum/helpers/variant_props.rs b/sea-orm-macros/src/strum/helpers/variant_props.rs index 3b4d201ffb..69cabe64dd 100644 --- a/sea-orm-macros/src/strum/helpers/variant_props.rs +++ b/sea-orm-macros/src/strum/helpers/variant_props.rs @@ -1,7 +1,7 @@ use std::default::Default; use syn::{Ident, LitStr, Variant}; -use super::metadata::{kw, VariantExt, VariantMeta}; +use super::metadata::{VariantExt, VariantMeta, kw}; use super::occurrence_error; pub trait HasStrumVariantProperties { diff --git a/sea-orm-macros/src/strum/mod.rs b/sea-orm-macros/src/strum/mod.rs index 5f9c70722e..dcf048d159 100644 --- a/sea-orm-macros/src/strum/mod.rs +++ b/sea-orm-macros/src/strum/mod.rs @@ -1,4 +1,6 @@ //! Source code adapted from https://github.com/Peternator7/strum +#![allow(dead_code)] + pub mod enum_iter; pub mod helpers; diff --git a/sea-orm-macros/tests/derive_active_enum_test.rs b/sea-orm-macros/tests/derive_active_enum_test.rs new file mode 100644 index 0000000000..f685191cc7 --- /dev/null +++ b/sea-orm-macros/tests/derive_active_enum_test.rs @@ -0,0 +1,154 @@ +use sea_orm::{ActiveEnum, entity::prelude::StringLen}; +use sea_orm_macros::{DeriveActiveEnum, EnumIter}; + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "test_enum", + rename_all = "camelCase" +)] +enum TestEnum { + DefaultVariant, + #[sea_orm(rename = "camelCase")] + VariantCamelCase, + #[sea_orm(rename = "kebab-case")] + VariantKebabCase, + #[sea_orm(rename = "mixed_case")] + VariantMixedCase, + #[sea_orm(rename = "SCREAMING_SNAKE_CASE")] + VariantShoutySnakeCase, + #[sea_orm(rename = "snake_case")] + VariantSnakeCase, + #[sea_orm(rename = "title_case")] + VariantTitleCase, + #[sea_orm(rename = "UPPERCASE")] + VariantUpperCase, + #[sea_orm(rename = "lowercase")] + VariantLowerCase, + #[sea_orm(rename = "SCREAMING-KEBAB-CASE")] + VariantScreamingKebabCase, + #[sea_orm(rename = "PascalCase")] + VariantPascalCase, + #[sea_orm(string_value = "CuStOmStRiNgVaLuE")] + CustomStringValue, +} + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "test_enum", + rename_all = "camelCase" +)] +enum TestRenameAllWithoutCasesEnum { + HelloWorld, +} + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "String", + db_type = "String(StringLen::None)", + rename_all = "snake_case" +)] +pub enum TestEnum2 { + HelloWorld, + #[sea_orm(rename = "camelCase")] + HelloWorldTwo, +} + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "String", + db_type = "String(StringLen::None)", + rename_all = "snake_case" +)] +pub enum TestEnum3 { + HelloWorld, +} + +#[test] +fn derive_active_enum_value() { + assert_eq!(TestEnum::DefaultVariant.to_value(), "defaultVariant"); + assert_eq!(TestEnum::VariantCamelCase.to_value(), "variantCamelCase"); + assert_eq!(TestEnum::VariantKebabCase.to_value(), "variant-kebab-case"); + assert_eq!(TestEnum::VariantMixedCase.to_value(), "variantMixedCase"); + assert_eq!( + TestEnum::VariantShoutySnakeCase.to_value(), + "VARIANT_SHOUTY_SNAKE_CASE" + ); + assert_eq!(TestEnum::VariantSnakeCase.to_value(), "variant_snake_case"); + assert_eq!(TestEnum::VariantTitleCase.to_value(), "Variant Title Case"); + assert_eq!(TestEnum::VariantUpperCase.to_value(), "VARIANTUPPERCASE"); + assert_eq!(TestEnum::VariantLowerCase.to_value(), "variantlowercase"); + assert_eq!( + TestEnum::VariantScreamingKebabCase.to_value(), + "VARIANT-SCREAMING-KEBAB-CASE" + ); + assert_eq!(TestEnum::VariantPascalCase.to_value(), "VariantPascalCase"); + assert_eq!(TestEnum::CustomStringValue.to_value(), "CuStOmStRiNgVaLuE"); +} + +#[test] +fn derive_active_enum_from_value() { + assert_eq!( + TestEnum::try_from_value(&"defaultVariant".to_string()), + Ok(TestEnum::DefaultVariant) + ); + assert_eq!( + TestEnum::try_from_value(&"variantCamelCase".to_string()), + Ok(TestEnum::VariantCamelCase) + ); + assert_eq!( + TestEnum::try_from_value(&"variant-kebab-case".to_string()), + Ok(TestEnum::VariantKebabCase) + ); + assert_eq!( + TestEnum::try_from_value(&"variantMixedCase".to_string()), + Ok(TestEnum::VariantMixedCase) + ); + assert_eq!( + TestEnum::try_from_value(&"VARIANT_SHOUTY_SNAKE_CASE".to_string()), + Ok(TestEnum::VariantShoutySnakeCase), + ); + assert_eq!( + TestEnum::try_from_value(&"variant_snake_case".to_string()), + Ok(TestEnum::VariantSnakeCase) + ); + assert_eq!( + TestEnum::try_from_value(&"Variant Title Case".to_string()), + Ok(TestEnum::VariantTitleCase) + ); + assert_eq!( + TestEnum::try_from_value(&"VARIANTUPPERCASE".to_string()), + Ok(TestEnum::VariantUpperCase) + ); + assert_eq!( + TestEnum::try_from_value(&"variantlowercase".to_string()), + Ok(TestEnum::VariantLowerCase) + ); + assert_eq!( + TestEnum::try_from_value(&"VARIANT-SCREAMING-KEBAB-CASE".to_string()), + Ok(TestEnum::VariantScreamingKebabCase), + ); + assert_eq!( + TestEnum::try_from_value(&"VariantPascalCase".to_string()), + Ok(TestEnum::VariantPascalCase) + ); + assert_eq!( + TestEnum::try_from_value(&"CuStOmStRiNgVaLuE".to_string()), + Ok(TestEnum::CustomStringValue) + ); +} + +#[test] +fn derive_active_enum_value_2() { + assert_eq!(TestEnum2::HelloWorld.to_value(), "hello_world"); + assert_eq!(TestEnum2::HelloWorldTwo.to_value(), "helloWorldTwo"); + + assert_eq!(TestEnum3::HelloWorld.to_value(), "hello_world"); + assert_eq!( + TestRenameAllWithoutCasesEnum::HelloWorld.to_value(), + "helloWorld" + ); +} diff --git a/sea-orm-macros/tests/derive_entity_model_auto_increment_test.rs b/sea-orm-macros/tests/derive_entity_model_auto_increment_test.rs new file mode 100644 index 0000000000..04c55282a0 --- /dev/null +++ b/sea-orm-macros/tests/derive_entity_model_auto_increment_test.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; +use sea_orm_macros::DeriveEntityModel; + +mod string_pk { + use super::*; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "string_pk")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +mod string_pk_set_true { + use super::*; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "string_pk")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = true)] + pub id: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +#[test] +fn test_auto_increment_default_by_type() { + assert!(!string_pk::PrimaryKey::auto_increment()); + assert!(string_pk_set_true::PrimaryKey::auto_increment()); +} diff --git a/sea-orm-macros/tests/derive_entity_model_column_name_test.rs b/sea-orm-macros/tests/derive_entity_model_column_name_test.rs new file mode 100644 index 0000000000..bc821e472c --- /dev/null +++ b/sea-orm-macros/tests/derive_entity_model_column_name_test.rs @@ -0,0 +1,58 @@ +use std::str::FromStr; + +use sea_orm::Iden; +use sea_orm::Iterable; +use sea_orm::prelude::*; +use sea_orm_macros::DeriveEntityModel; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user", rename_all = "camelCase")] +pub struct Model { + #[sea_orm(primary_key)] + id: i32, + username: String, + first_name: String, + middle_name: String, + #[sea_orm(column_name = "lAsTnAmE")] + last_name: String, + orders_count: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +#[test] +fn test_column_names() { + let columns: Vec = Column::iter().map(|item| item.to_string()).collect(); + + assert_eq!( + columns, + vec![ + "id", + "username", + "firstName", + "middleName", + "lAsTnAmE", + "ordersCount", + ] + ); + + let col = + Column::from_str("firstName").expect("column from str should recognize column_name attr"); + assert!(matches!(col, Column::FirstName)); + let col = + Column::from_str("first_name").expect("column from str should recognize column_name attr"); + assert!(matches!(col, Column::FirstName)); + + let col = + Column::from_str("lastName").expect("column from str should recognize column_name attr"); + assert!(matches!(col, Column::LastName)); + let col = + Column::from_str("last_name").expect("column from str should recognize column_name attr"); + assert!(matches!(col, Column::LastName)); + let col = + Column::from_str("lAsTnAmE").expect("column from str should recognize column_name attr"); + assert!(matches!(col, Column::LastName)); +} diff --git a/sea-orm-macros/tests/derive_value_type_test.rs b/sea-orm-macros/tests/derive_value_type_test.rs new file mode 100644 index 0000000000..7961ba13f1 --- /dev/null +++ b/sea-orm-macros/tests/derive_value_type_test.rs @@ -0,0 +1,79 @@ +#[test] +fn when_user_import_nothing_macro_still_works_test() { + #[derive(sea_orm::DeriveValueType)] + struct MyString(String); +} + +#[test] +fn when_user_alias_result_macro_still_works_test() { + #[allow(dead_code)] + type Result = std::result::Result; + #[derive(sea_orm::DeriveValueType)] + struct MyString(String); +} + +#[test] +fn when_stringy_newtype_works_test() { + #[allow(dead_code)] + #[derive(sea_orm::DeriveValueType)] + #[sea_orm(value_type = "String")] + struct Foo { + inner: i32, + } + + impl std::fmt::Display for Foo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.fmt(f) + } + } + + impl std::str::FromStr for Foo { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Self { inner: s.parse()? }) + } + } +} + +#[test] +fn when_explicit_stringy_newtype_works_test() { + #[derive(sea_orm::DeriveValueType)] + #[sea_orm(value_type = "String")] + struct Foo(i32); + + impl std::fmt::Display for Foo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl std::str::FromStr for Foo { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?)) + } + } +} + +#[test] +fn when_custom_from_str_works() { + #[derive(sea_orm::DeriveValueType)] + #[sea_orm( + value_type = "String", + from_str = "Foo::from_str", + to_str = "Foo::to_str" + )] + struct Foo(i32); + + impl Foo { + fn from_str(_s: &str) -> Result { + Ok(Self(42)) + } + + fn to_str(&self) -> String { + 42.to_string() + } + } +} diff --git a/sea-orm-migration/Cargo.toml b/sea-orm-migration/Cargo.toml index a77eb316cc..2e403b65c8 100644 --- a/sea-orm-migration/Cargo.toml +++ b/sea-orm-migration/Cargo.toml @@ -2,18 +2,18 @@ # A separate workspace [package] -name = "sea-orm-migration" -version = "0.12.0" -authors = ["Billy Chan "] -edition = "2021" -description = "Migration utility for SeaORM" -license = "MIT OR Apache-2.0" -homepage = "https://www.sea-ql.org/SeaORM" +authors = ["Billy Chan "] +categories = ["database"] +description = "Migration utility for SeaORM" documentation = "https://docs.rs/sea-orm" -repository = "https://github.com/SeaQL/sea-orm" -categories = ["database"] -keywords = ["async", "orm", "mysql", "postgres", "sqlite"] -rust-version = "1.65" +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" +keywords = ["async", "orm", "mysql", "postgres", "sqlite"] +license = "MIT OR Apache-2.0" +name = "sea-orm-migration" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.37" [lib] name = "sea_orm_migration" @@ -21,27 +21,77 @@ path = "src/lib.rs" [dependencies] async-trait = { version = "0.1", default-features = false } -clap = { version = "3.2", default-features = false, features = ["std", "env", "derive"], optional = true } +clap = { version = "4.3", features = ["env", "derive"], optional = true } dotenvy = { version = "0.15", default-features = false, optional = true } -sea-orm = { version = "0.12.0", path = "../", default-features = false, features = ["macros"] } -sea-orm-cli = { version = "0.12.0", path = "../sea-orm-cli", default-features = false, optional = true } -sea-schema = { version = "0.12.0-rc.2" } +sea-orm = { version = "~2.0.0-rc.37", path = "../", features = [ + "schema-sync", +] } +sea-orm-cli = { version = "~2.0.0-rc.37", path = "../sea-orm-cli", default-features = false, optional = true } +sea-schema = { version = "0.17.0-rc", default-features = false, features = [ + "discovery", + "writer", + "probe", +] } tracing = { version = "0.1", default-features = false, features = ["log"] } -tracing-subscriber = { version = "0.3.17", default-features = false, features = ["env-filter", "fmt"] } -futures = { version = "0.3", default-features = false, features = ["std"] } +tracing-subscriber = { version = "0.3.17", default-features = false, features = [ + "env-filter", + "fmt", +] } [dev-dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } [features] -default = ["cli"] cli = ["clap", "dotenvy", "sea-orm-cli/cli"] -sqlx-mysql = ["sea-orm/sqlx-mysql"] -sqlx-postgres = ["sea-orm/sqlx-postgres"] -sqlx-sqlite = ["sea-orm/sqlx-sqlite"] -runtime-actix-native-tls = ["sea-orm/runtime-actix-native-tls"] -runtime-async-std-native-tls = ["sea-orm/runtime-async-std-native-tls"] -runtime-tokio-native-tls = ["sea-orm/runtime-tokio-native-tls"] -runtime-actix-rustls = ["sea-orm/runtime-actix-rustls"] -runtime-async-std-rustls = ["sea-orm/runtime-async-std-rustls"] -runtime-tokio-rustls = ["sea-orm/runtime-tokio-rustls"] +default = ["cli"] +entity-registry = ["sea-orm/entity-registry"] +runtime-async-std = [ + "sea-orm/runtime-async-std", + "sea-schema/runtime-async-std", + "sea-orm-cli?/runtime-async-std", +] +runtime-async-std-native-tls = [ + "sea-orm/runtime-async-std-native-tls", + "sea-schema/runtime-async-std-native-tls", + "sea-orm-cli?/runtime-async-std-native-tls", +] +runtime-async-std-rustls = [ + "sea-orm/runtime-async-std-rustls", + "sea-schema/runtime-async-std-rustls", + "sea-orm-cli?/runtime-async-std-rustls", +] +runtime-tokio = [ + "sea-orm/runtime-tokio", + "sea-schema/runtime-tokio", + "sea-orm-cli?/runtime-tokio", +] +runtime-tokio-native-tls = [ + "sea-orm/runtime-tokio-native-tls", + "sea-schema/runtime-tokio-native-tls", + "sea-orm-cli?/runtime-tokio-native-tls", +] +runtime-tokio-rustls = [ + "sea-orm/runtime-tokio-rustls", + "sea-schema/runtime-tokio-rustls", + "sea-orm-cli?/runtime-tokio-rustls", +] +sqlite-use-returning-for-3_35 = ["sea-orm/sqlite-use-returning-for-3_35"] +sqlx-all = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite"] +sqlx-dep = [] +sqlx-mysql = ["sqlx-dep", "sea-orm/sqlx-mysql", "sea-orm-cli?/sqlx-mysql"] +sqlx-postgres = [ + "sqlx-dep", + "sea-orm/sqlx-postgres", + "sea-orm-cli?/sqlx-postgres", +] +sqlx-sqlite = ["sqlx-dep", "sea-orm/sqlx-sqlite", "sea-orm-cli?/sqlx-sqlite"] +with-bigdecimal = ["sea-orm/with-bigdecimal"] +with-chrono = ["sea-orm/with-chrono"] +with-ipnetwork = ["sea-orm/with-ipnetwork"] +with-json = ["sea-orm/with-json"] +with-rust_decimal = ["sea-orm/with-rust_decimal"] +with-time = ["sea-orm/with-time"] +with-uuid = ["sea-orm/with-uuid"] + +[patch.crates-io] +# sea-query = { path = "../sea-query" } diff --git a/sea-orm-migration/src/cli.rs b/sea-orm-migration/src/cli.rs index a7a72539f5..6222009c21 100644 --- a/sea-orm-migration/src/cli.rs +++ b/sea-orm-migration/src/cli.rs @@ -1,18 +1,34 @@ +use std::future::Future; + use clap::Parser; use dotenvy::dotenv; use std::{error::Error, fmt::Display, process::exit}; -use tracing_subscriber::{prelude::*, EnvFilter}; +use tracing_subscriber::{EnvFilter, prelude::*}; -use sea_orm::{ConnectOptions, Database, DbConn}; -use sea_orm_cli::{run_migrate_generate, run_migrate_init, MigrateSubcommands}; +use sea_orm::{ConnectOptions, Database, DbConn, DbErr}; +use sea_orm_cli::{MigrateSubcommands, run_migrate_generate, run_migrate_init}; -use super::MigratorTrait; +use super::MigratorTraitSelf; const MIGRATION_DIR: &str = "./"; pub async fn run_cli(migrator: M) where - M: MigratorTrait, + M: MigratorTraitSelf, +{ + run_cli_with_connection(migrator, Database::connect).await; +} + +/// Same as [`run_cli`] where you provide the function to create the [`DbConn`]. +/// +/// This allows configuring the database connection as you see fit. +/// E.g. you can change settings in [`ConnectOptions`] or you can load sqlite +/// extensions. +pub async fn run_cli_with_connection(migrator: M, make_connection: F) +where + M: MigratorTraitSelf, + F: FnOnce(ConnectOptions) -> Fut, + Fut: Future>, { dotenv().ok(); let cli = Cli::parse(); @@ -25,23 +41,24 @@ where let connect_options = ConnectOptions::new(url) .set_schema_search_path(schema) .to_owned(); - let db = &Database::connect(connect_options) + + let db = make_connection(connect_options) .await .expect("Fail to acquire database connection"); - run_migrate(migrator, db, cli.command, cli.verbose) + run_migrate(migrator, &db, cli.command, cli.verbose) .await .unwrap_or_else(handle_error); } pub async fn run_migrate( - _: M, + migrator: M, db: &DbConn, command: Option, verbose: bool, ) -> Result<(), Box> where - M: MigratorTrait, + M: MigratorTraitSelf, { let filter = match verbose { true => "debug", @@ -68,32 +85,31 @@ where }; match command { - Some(MigrateSubcommands::Fresh) => M::fresh(db).await?, - Some(MigrateSubcommands::Refresh) => M::refresh(db).await?, - Some(MigrateSubcommands::Reset) => M::reset(db).await?, - Some(MigrateSubcommands::Status) => M::status(db).await?, - Some(MigrateSubcommands::Up { num }) => M::up(db, num).await?, - Some(MigrateSubcommands::Down { num }) => M::down(db, Some(num)).await?, + Some(MigrateSubcommands::Fresh) => migrator.fresh(db).await?, + Some(MigrateSubcommands::Refresh) => migrator.refresh(db).await?, + Some(MigrateSubcommands::Reset) => migrator.reset(db).await?, + Some(MigrateSubcommands::Status) => migrator.status(db).await?, + Some(MigrateSubcommands::Up { num }) => migrator.up(db, num).await?, + Some(MigrateSubcommands::Down { num }) => migrator.down(db, Some(num)).await?, Some(MigrateSubcommands::Init) => run_migrate_init(MIGRATION_DIR)?, Some(MigrateSubcommands::Generate { migration_name, universal_time: _, local_time, }) => run_migrate_generate(MIGRATION_DIR, &migration_name, !local_time)?, - _ => M::up(db, None).await?, + _ => migrator.up(db, None).await?, }; Ok(()) } #[derive(Parser)] -#[clap(version)] +#[command(version)] pub struct Cli { - #[clap(action, short = 'v', long, global = true, help = "Show debug messages")] + #[arg(short = 'v', long, global = true, help = "Show debug messages")] verbose: bool, - #[clap( - value_parser, + #[arg( global = true, short = 's', long, @@ -104,8 +120,7 @@ pub struct Cli { )] database_schema: Option, - #[clap( - value_parser, + #[arg( global = true, short = 'u', long, @@ -114,7 +129,7 @@ pub struct Cli { )] database_url: Option, - #[clap(subcommand)] + #[command(subcommand)] command: Option, } diff --git a/sea-orm-migration/src/connection.rs b/sea-orm-migration/src/connection.rs index 116185e45e..78cbd71856 100644 --- a/sea-orm-migration/src/connection.rs +++ b/sea-orm-migration/src/connection.rs @@ -1,148 +1,4 @@ -use futures::Future; -use sea_orm::{ - AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, - ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait, +pub use sea_orm::{ + DatabaseExecutor as SchemaManagerConnection, + IntoDatabaseExecutor as IntoSchemaManagerConnection, }; -use std::pin::Pin; - -pub enum SchemaManagerConnection<'c> { - Connection(&'c DatabaseConnection), - Transaction(&'c DatabaseTransaction), -} - -#[async_trait::async_trait] -impl<'c> ConnectionTrait for SchemaManagerConnection<'c> { - fn get_database_backend(&self) -> DbBackend { - match self { - SchemaManagerConnection::Connection(conn) => conn.get_database_backend(), - SchemaManagerConnection::Transaction(trans) => trans.get_database_backend(), - } - } - - async fn execute(&self, stmt: Statement) -> Result { - match self { - SchemaManagerConnection::Connection(conn) => conn.execute(stmt).await, - SchemaManagerConnection::Transaction(trans) => trans.execute(stmt).await, - } - } - - async fn execute_unprepared(&self, sql: &str) -> Result { - match self { - SchemaManagerConnection::Connection(conn) => conn.execute_unprepared(sql).await, - SchemaManagerConnection::Transaction(trans) => trans.execute_unprepared(sql).await, - } - } - - async fn query_one(&self, stmt: Statement) -> Result, DbErr> { - match self { - SchemaManagerConnection::Connection(conn) => conn.query_one(stmt).await, - SchemaManagerConnection::Transaction(trans) => trans.query_one(stmt).await, - } - } - - async fn query_all(&self, stmt: Statement) -> Result, DbErr> { - match self { - SchemaManagerConnection::Connection(conn) => conn.query_all(stmt).await, - SchemaManagerConnection::Transaction(trans) => trans.query_all(stmt).await, - } - } - - fn is_mock_connection(&self) -> bool { - match self { - SchemaManagerConnection::Connection(conn) => conn.is_mock_connection(), - SchemaManagerConnection::Transaction(trans) => trans.is_mock_connection(), - } - } -} - -#[async_trait::async_trait] -impl<'c> TransactionTrait for SchemaManagerConnection<'c> { - async fn begin(&self) -> Result { - match self { - SchemaManagerConnection::Connection(conn) => conn.begin().await, - SchemaManagerConnection::Transaction(trans) => trans.begin().await, - } - } - - async fn begin_with_config( - &self, - isolation_level: Option, - access_mode: Option, - ) -> Result { - match self { - SchemaManagerConnection::Connection(conn) => { - conn.begin_with_config(isolation_level, access_mode).await - } - SchemaManagerConnection::Transaction(trans) => { - trans.begin_with_config(isolation_level, access_mode).await - } - } - } - - async fn transaction(&self, callback: F) -> Result> - where - F: for<'a> FnOnce( - &'a DatabaseTransaction, - ) -> Pin> + Send + 'a>> - + Send, - T: Send, - E: std::error::Error + Send, - { - match self { - SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await, - SchemaManagerConnection::Transaction(trans) => trans.transaction(callback).await, - } - } - - async fn transaction_with_config( - &self, - callback: F, - isolation_level: Option, - access_mode: Option, - ) -> Result> - where - F: for<'a> FnOnce( - &'a DatabaseTransaction, - ) -> Pin> + Send + 'a>> - + Send, - T: Send, - E: std::error::Error + Send, - { - match self { - SchemaManagerConnection::Connection(conn) => { - conn.transaction_with_config(callback, isolation_level, access_mode) - .await - } - SchemaManagerConnection::Transaction(trans) => { - trans - .transaction_with_config(callback, isolation_level, access_mode) - .await - } - } - } -} - -pub trait IntoSchemaManagerConnection<'c>: Send -where - Self: 'c, -{ - fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c>; -} - -impl<'c> IntoSchemaManagerConnection<'c> for SchemaManagerConnection<'c> { - fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { - self - } -} - -impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseConnection { - fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { - SchemaManagerConnection::Connection(self) - } -} - -impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseTransaction { - fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { - SchemaManagerConnection::Transaction(self) - } -} diff --git a/sea-orm-migration/src/lib.rs b/sea-orm-migration/src/lib.rs index 2118a20d4f..4834393c97 100644 --- a/sea-orm-migration/src/lib.rs +++ b/sea-orm-migration/src/lib.rs @@ -4,6 +4,7 @@ pub mod connection; pub mod manager; pub mod migrator; pub mod prelude; +pub mod schema; pub mod seaql_migrations; pub mod util; @@ -13,8 +14,8 @@ pub use migrator::*; pub use async_trait; pub use sea_orm; -pub use sea_orm::sea_query; pub use sea_orm::DbErr; +pub use sea_orm::sea_query; pub trait MigrationName { fn name(&self) -> &str; @@ -30,4 +31,13 @@ pub trait MigrationTrait: MigrationName + Send + Sync { async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { Err(DbErr::Migration("We Don't Do That Here".to_owned())) } + + /// Control whether this migration runs inside a transaction. + /// + /// - `None` (default): follow backend convention (Postgres = transaction, MySQL/SQLite = no transaction) + /// - `Some(true)`: force wrapping in a transaction on any backend + /// - `Some(false)`: disable automatic transaction wrapping (use `manager.begin()` for manual control) + fn use_transaction(&self) -> Option { + None + } } diff --git a/sea-orm-migration/src/manager.rs b/sea-orm-migration/src/manager.rs index f70c9212ac..6817ff9838 100644 --- a/sea-orm-migration/src/manager.rs +++ b/sea-orm-migration/src/manager.rs @@ -1,12 +1,13 @@ use super::{IntoSchemaManagerConnection, SchemaManagerConnection}; use sea_orm::sea_query::{ - extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, - TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, - TableTruncateStatement, + SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement, + TableRenameStatement, TableTruncateStatement, + extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, }; -use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder}; -use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; +use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder, TransactionTrait}; +#[allow(unused_imports)] +use sea_schema::probe::SchemaProbe; /// Helper struct for writing migration scripts in migration file pub struct SchemaManager<'c> { @@ -19,16 +20,23 @@ impl<'c> SchemaManager<'c> { T: IntoSchemaManagerConnection<'c>, { Self { - conn: conn.into_schema_manager_connection(), + conn: conn.into_database_executor(), } } + pub async fn execute(&self, stmt: S) -> Result<(), DbErr> + where + S: StatementBuilder, + { + self.conn.execute(&stmt).await.map(|_| ()) + } + + #[doc(hidden)] pub async fn exec_stmt(&self, stmt: S) -> Result<(), DbErr> where S: StatementBuilder, { - let builder = self.conn.get_database_backend(); - self.conn.execute(builder.build(&stmt)).await.map(|_| ()) + self.conn.execute(&stmt).await.map(|_| ()) } pub fn get_database_backend(&self) -> DbBackend { @@ -40,100 +48,182 @@ impl<'c> SchemaManager<'c> { } } +/// Transaction Control +impl SchemaManager<'_> { + /// Begin a new transaction, returning an owned `SchemaManager` backed by it. + /// + /// Useful in migrations with `use_transaction() -> Some(false)` for manual + /// transaction management (e.g., separating DDL and DML into distinct transactions). + pub async fn begin(&self) -> Result, DbErr> { + let txn = self.conn.begin().await?; + Ok(SchemaManager { + conn: SchemaManagerConnection::OwnedTransaction(txn), + }) + } + + /// Commit the owned transaction. Only valid on a `SchemaManager` created by [`begin()`](Self::begin). + pub async fn commit(self) -> Result<(), DbErr> { + match self.conn { + SchemaManagerConnection::OwnedTransaction(txn) => txn.commit().await, + _ => Err(DbErr::Custom( + "Cannot commit: SchemaManager does not own a transaction".into(), + )), + } + } +} + /// Schema Creation -impl<'c> SchemaManager<'c> { +impl SchemaManager<'_> { pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } } /// Schema Mutation -impl<'c> SchemaManager<'c> { +impl SchemaManager<'_> { pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> { - self.exec_stmt(stmt).await + self.execute(stmt).await } } -/// Schema Inspection -impl<'c> SchemaManager<'c> { +/// Schema Inspection. +impl SchemaManager<'_> { pub async fn has_table(&self, table: T) -> Result where T: AsRef, { - let stmt = match self.conn.get_database_backend() { - DbBackend::MySql => MySql::has_table(table), - DbBackend::Postgres => Postgres::has_table(table), - DbBackend::Sqlite => Sqlite::has_table(table), + has_table(&self.conn, table).await + } + + pub async fn has_column(&self, _table: T, _column: C) -> Result + where + T: AsRef, + C: AsRef, + { + let _stmt: SelectStatement = match self.conn.get_database_backend() { + #[cfg(feature = "sqlx-mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.has_column(_table, _column), + #[cfg(feature = "sqlx-postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.has_column(_table, _column), + #[cfg(feature = "sqlx-sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_column(_table, _column), + #[allow(unreachable_patterns)] + other => { + return Err(DbErr::BackendNotSupported { + db: other.as_str(), + ctx: "has_column", + }); + } }; - let builder = self.conn.get_database_backend(); + #[allow(unreachable_code)] let res = self .conn - .query_one(builder.build(&stmt)) + .query_one(&_stmt) .await? - .ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?; + .ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?; - res.try_get("", "has_table") + res.try_get("", "has_column") } - pub async fn has_column(&self, table: T, column: C) -> Result + pub async fn has_index(&self, _table: T, _index: I) -> Result where T: AsRef, - C: AsRef, + I: AsRef, { - let stmt = match self.conn.get_database_backend() { - DbBackend::MySql => MySql::has_column(table, column), - DbBackend::Postgres => Postgres::has_column(table, column), - DbBackend::Sqlite => Sqlite::has_column(table, column), + let _stmt: SelectStatement = match self.conn.get_database_backend() { + #[cfg(feature = "sqlx-mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.has_index(_table, _index), + #[cfg(feature = "sqlx-postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.has_index(_table, _index), + #[cfg(feature = "sqlx-sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_index(_table, _index), + #[allow(unreachable_patterns)] + other => { + return Err(DbErr::BackendNotSupported { + db: other.as_str(), + ctx: "has_index", + }); + } }; - let builder = self.conn.get_database_backend(); + #[allow(unreachable_code)] let res = self .conn - .query_one(builder.build(&stmt)) + .query_one(&_stmt) .await? - .ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?; + .ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?; - res.try_get("", "has_column") + res.try_get("", "has_index") } } + +pub(crate) async fn has_table(conn: &C, _table: T) -> Result +where + C: ConnectionTrait, + T: AsRef, +{ + let _stmt: SelectStatement = match conn.get_database_backend() { + #[cfg(feature = "sqlx-mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.has_table(_table), + #[cfg(feature = "sqlx-postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.has_table(_table), + #[cfg(feature = "sqlx-sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_table(_table), + #[allow(unreachable_patterns)] + other => { + return Err(DbErr::BackendNotSupported { + db: other.as_str(), + ctx: "has_table", + }); + } + }; + + #[allow(unreachable_code)] + let res = conn + .query_one(&_stmt) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?; + + res.try_get("", "has_table") +} diff --git a/sea-orm-migration/src/migrator.rs b/sea-orm-migration/src/migrator.rs index 84adb17f1e..b9dc7f99f6 100644 --- a/sea-orm-migration/src/migrator.rs +++ b/sea-orm-migration/src/migrator.rs @@ -1,21 +1,17 @@ -use futures::Future; -use std::collections::HashSet; +mod queries; + +mod exec; +use exec::*; + +mod with_self; +pub use with_self::*; + use std::fmt::Display; -use std::pin::Pin; -use std::time::SystemTime; use tracing::info; -use sea_orm::sea_query::{ - self, extension::postgres::Type, Alias, Expr, ForeignKey, Iden, IntoIden, JoinType, Order, - Query, SelectStatement, SimpleExpr, Table, -}; -use sea_orm::{ - ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DynIden, - EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement, TransactionTrait, -}; -use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; - -use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager}; +use super::{IntoSchemaManagerConnection, MigrationTrait, SchemaManager, seaql_migrations}; +use sea_orm::sea_query::IntoIden; +use sea_orm::{ConnectionTrait, DbBackend, DbErr, DynIden, TransactionTrait}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// Status of migration @@ -81,15 +77,7 @@ pub trait MigratorTrait: Send { C: ConnectionTrait, { Self::install(db).await?; - let stmt = Query::select() - .table_name(Self::migration_table_name()) - .columns(seaql_migrations::Column::iter().map(IntoIden::into_iden)) - .order_by(seaql_migrations::Column::Version, Order::Asc) - .to_owned(); - let builder = db.get_database_backend(); - seaql_migrations::Model::find_by_statement(builder.build(&stmt)) - .all(db) - .await + get_migration_models(db, Self::migration_table_name()).await } /// Get list of migrations with status @@ -98,37 +86,10 @@ pub trait MigratorTrait: Send { C: ConnectionTrait, { Self::install(db).await?; - let mut migration_files = Self::get_migration_files(); - let migration_models = Self::get_migration_models(db).await?; - - let migration_in_db: HashSet = migration_models - .into_iter() - .map(|model| model.version) - .collect(); - let migration_in_fs: HashSet = migration_files - .iter() - .map(|file| file.migration.name().to_string()) - .collect(); - - let pending_migrations = &migration_in_fs - &migration_in_db; - for migration_file in migration_files.iter_mut() { - if !pending_migrations.contains(migration_file.migration.name()) { - migration_file.status = MigrationStatus::Applied; - } - } - - let missing_migrations_in_fs = &migration_in_db - &migration_in_fs; - let errors: Vec = missing_migrations_in_fs - .iter() - .map(|missing_migration| { - format!("Migration file of version '{missing_migration}' is missing, this migration has been applied but its file is missing") - }).collect(); - - if !errors.is_empty() { - Err(DbErr::Custom(errors.join("\n"))) - } else { - Ok(migration_files) - } + get_migration_with_status( + Self::get_migration_files(), + Self::get_migration_models(db).await?, + ) } /// Get list of pending migrations @@ -162,13 +123,7 @@ pub trait MigratorTrait: Send { where C: ConnectionTrait, { - let builder = db.get_database_backend(); - let schema = Schema::new(builder); - let mut stmt = schema - .create_table_from_entity(seaql_migrations::Entity) - .table_name(Self::migration_table_name()); - stmt.if_not_exists(); - db.execute(builder.build(&stmt)).await.map(|_| ()) + install(db, Self::migration_table_name()).await } /// Check the status of all migrations @@ -192,10 +147,7 @@ pub trait MigratorTrait: Send { where C: IntoSchemaManagerConnection<'c>, { - exec_with_connection::<'_, _, _>(db, move |manager| { - Box::pin(async move { exec_fresh::(manager).await }) - }) - .await + exec_with_connection!(db, async |manager| { exec_fresh::(manager).await }).await } /// Rollback all applied migrations, then reapply all migrations @@ -203,11 +155,9 @@ pub trait MigratorTrait: Send { where C: IntoSchemaManagerConnection<'c>, { - exec_with_connection::<'_, _, _>(db, move |manager| { - Box::pin(async move { - exec_down::(manager, None).await?; - exec_up::(manager, None).await - }) + exec_with_connection!(db, async |manager| { + exec_down::(manager, None).await?; + exec_up::(manager, None).await }) .await } @@ -217,55 +167,46 @@ pub trait MigratorTrait: Send { where C: IntoSchemaManagerConnection<'c>, { - exec_with_connection::<'_, _, _>(db, move |manager| { - Box::pin(async move { exec_down::(manager, None).await }) + exec_with_connection!(db, async |manager| { + // Rollback all applied migrations first + exec_down::(manager, None).await?; + + // Then drop the migration table itself + uninstall(manager, Self::migration_table_name()).await }) .await } - /// Apply pending migrations - async fn up<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + /// Uninstall migration tracking table only (non-destructive) + /// This will drop the `seaql_migrations` table but won't rollback other schema changes. + async fn uninstall<'c, C>(db: C) -> Result<(), DbErr> where C: IntoSchemaManagerConnection<'c>, { - exec_with_connection::<'_, _, _>(db, move |manager| { - Box::pin(async move { exec_up::(manager, steps).await }) + exec_with_connection!(db, async |manager| { + uninstall(manager, Self::migration_table_name()).await }) .await } - /// Rollback applied migrations - async fn down<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + /// Apply pending migrations + async fn up<'c, C>(db: C, steps: Option) -> Result<(), DbErr> where C: IntoSchemaManagerConnection<'c>, { - exec_with_connection::<'_, _, _>(db, move |manager| { - Box::pin(async move { exec_down::(manager, steps).await }) - }) - .await + let db = db.into_database_executor(); + let manager = SchemaManager::new(db); + exec_up::(&manager, steps).await } -} -async fn exec_with_connection<'c, C, F>(db: C, f: F) -> Result<(), DbErr> -where - C: IntoSchemaManagerConnection<'c>, - F: for<'b> Fn( - &'b SchemaManager<'_>, - ) -> Pin> + Send + 'b>>, -{ - let db = db.into_schema_manager_connection(); - - match db.get_database_backend() { - DbBackend::Postgres => { - let transaction = db.begin().await?; - let manager = SchemaManager::new(&transaction); - f(&manager).await?; - transaction.commit().await - } - DbBackend::MySql | DbBackend::Sqlite => { - let manager = SchemaManager::new(db); - f(&manager).await - } + /// Rollback applied migrations + async fn down<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + let db = db.into_database_executor(); + let manager = SchemaManager::new(db); + exec_down::(&manager, steps).await } } @@ -276,85 +217,13 @@ where let db = manager.get_connection(); M::install(db).await?; - let db_backend = db.get_database_backend(); - - // Temporarily disable the foreign key check - if db_backend == DbBackend::Sqlite { - info!("Disabling foreign key check"); - db.execute(Statement::from_string( - db_backend, - "PRAGMA foreign_keys = OFF".to_owned(), - )) - .await?; - info!("Foreign key check disabled"); - } - - // Drop all foreign keys - if db_backend == DbBackend::MySql { - info!("Dropping all foreign keys"); - let stmt = query_mysql_foreign_keys(db); - let rows = db.query_all(db_backend.build(&stmt)).await?; - for row in rows.into_iter() { - let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?; - let table_name: String = row.try_get("", "TABLE_NAME")?; - info!( - "Dropping foreign key '{}' from table '{}'", - constraint_name, table_name - ); - let mut stmt = ForeignKey::drop(); - stmt.table(Alias::new(table_name.as_str())) - .name(constraint_name.as_str()); - db.execute(db_backend.build(&stmt)).await?; - info!("Foreign key '{}' has been dropped", constraint_name); - } - info!("All foreign keys dropped"); - } - - // Drop all tables - let stmt = query_tables(db); - let rows = db.query_all(db_backend.build(&stmt)).await?; - for row in rows.into_iter() { - let table_name: String = row.try_get("", "table_name")?; - info!("Dropping table '{}'", table_name); - let mut stmt = Table::drop(); - stmt.table(Alias::new(table_name.as_str())) - .if_exists() - .cascade(); - db.execute(db_backend.build(&stmt)).await?; - info!("Table '{}' has been dropped", table_name); - } - - // Drop all types - if db_backend == DbBackend::Postgres { - info!("Dropping all types"); - let stmt = query_pg_types(db); - let rows = db.query_all(db_backend.build(&stmt)).await?; - for row in rows { - let type_name: String = row.try_get("", "typname")?; - info!("Dropping type '{}'", type_name); - let mut stmt = Type::drop(); - stmt.name(Alias::new(&type_name)); - db.execute(db_backend.build(&stmt)).await?; - info!("Type '{}' has been dropped", type_name); - } - } - // Restore the foreign key check - if db_backend == DbBackend::Sqlite { - info!("Restoring foreign key check"); - db.execute(Statement::from_string( - db_backend, - "PRAGMA foreign_keys = ON".to_owned(), - )) - .await?; - info!("Foreign key check restored"); - } + drop_everything(db).await?; - // Reapply all migrations exec_up::(manager, None).await } -async fn exec_up(manager: &SchemaManager<'_>, mut steps: Option) -> Result<(), DbErr> +async fn exec_up(manager: &SchemaManager<'_>, steps: Option) -> Result<(), DbErr> where M: MigratorTrait + ?Sized, { @@ -362,42 +231,16 @@ where M::install(db).await?; - if let Some(steps) = steps { - info!("Applying {} pending migrations", steps); - } else { - info!("Applying all pending migrations"); - } - - let migrations = M::get_pending_migrations(db).await?.into_iter(); - if migrations.len() == 0 { - info!("No pending migrations"); - } - for Migration { migration, .. } in migrations { - if let Some(steps) = steps.as_mut() { - if steps == &0 { - break; - } - *steps -= 1; - } - info!("Applying migration '{}'", migration.name()); - migration.up(manager).await?; - info!("Migration '{}' has been applied", migration.name()); - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("SystemTime before UNIX EPOCH!"); - seaql_migrations::Entity::insert(seaql_migrations::ActiveModel { - version: ActiveValue::Set(migration.name().to_owned()), - applied_at: ActiveValue::Set(now.as_secs() as i64), - }) - .table_name(M::migration_table_name()) - .exec(db) - .await?; - } - - Ok(()) + exec_up_with( + manager, + steps, + M::get_pending_migrations(db).await?, + M::migration_table_name(), + ) + .await } -async fn exec_down(manager: &SchemaManager<'_>, mut steps: Option) -> Result<(), DbErr> +async fn exec_down(manager: &SchemaManager<'_>, steps: Option) -> Result<(), DbErr> where M: MigratorTrait + ?Sized, { @@ -405,184 +248,11 @@ where M::install(db).await?; - if let Some(steps) = steps { - info!("Rolling back {} applied migrations", steps); - } else { - info!("Rolling back all applied migrations"); - } - - let migrations = M::get_applied_migrations(db).await?.into_iter().rev(); - if migrations.len() == 0 { - info!("No applied migrations"); - } - for Migration { migration, .. } in migrations { - if let Some(steps) = steps.as_mut() { - if steps == &0 { - break; - } - *steps -= 1; - } - info!("Rolling back migration '{}'", migration.name()); - migration.down(manager).await?; - info!("Migration '{}' has been rollbacked", migration.name()); - seaql_migrations::Entity::delete_many() - .filter(Expr::col(seaql_migrations::Column::Version).eq(migration.name())) - .table_name(M::migration_table_name()) - .exec(db) - .await?; - } - - Ok(()) -} - -fn query_tables(db: &C) -> SelectStatement -where - C: ConnectionTrait, -{ - match db.get_database_backend() { - DbBackend::MySql => MySql::query_tables(), - DbBackend::Postgres => Postgres::query_tables(), - DbBackend::Sqlite => Sqlite::query_tables(), - } -} - -fn get_current_schema(db: &C) -> SimpleExpr -where - C: ConnectionTrait, -{ - match db.get_database_backend() { - DbBackend::MySql => MySql::get_current_schema(), - DbBackend::Postgres => Postgres::get_current_schema(), - DbBackend::Sqlite => unimplemented!(), - } -} - -#[derive(Iden)] -enum InformationSchema { - #[iden = "information_schema"] - Schema, - #[iden = "TABLE_NAME"] - TableName, - #[iden = "CONSTRAINT_NAME"] - ConstraintName, - TableConstraints, - TableSchema, - ConstraintType, -} - -fn query_mysql_foreign_keys(db: &C) -> SelectStatement -where - C: ConnectionTrait, -{ - let mut stmt = Query::select(); - stmt.columns([ - InformationSchema::TableName, - InformationSchema::ConstraintName, - ]) - .from(( - InformationSchema::Schema, - InformationSchema::TableConstraints, - )) - .cond_where( - Condition::all() - .add(Expr::expr(get_current_schema(db)).equals(( - InformationSchema::TableConstraints, - InformationSchema::TableSchema, - ))) - .add( - Expr::col(( - InformationSchema::TableConstraints, - InformationSchema::ConstraintType, - )) - .eq("FOREIGN KEY"), - ), - ); - stmt -} - -#[derive(Iden)] -enum PgType { - Table, - Typname, - Typnamespace, - Typelem, -} - -#[derive(Iden)] -enum PgNamespace { - Table, - Oid, - Nspname, -} - -fn query_pg_types(db: &C) -> SelectStatement -where - C: ConnectionTrait, -{ - let mut stmt = Query::select(); - stmt.column(PgType::Typname) - .from(PgType::Table) - .join( - JoinType::LeftJoin, - PgNamespace::Table, - Expr::col((PgNamespace::Table, PgNamespace::Oid)) - .equals((PgType::Table, PgType::Typnamespace)), - ) - .cond_where( - Condition::all() - .add( - Expr::expr(get_current_schema(db)) - .equals((PgNamespace::Table, PgNamespace::Nspname)), - ) - .add(Expr::col((PgType::Table, PgType::Typelem)).eq(0)), - ); - stmt -} - -trait QueryTable { - type Statement; - - fn table_name(self, table_name: DynIden) -> Self::Statement; -} - -impl QueryTable for SelectStatement { - type Statement = SelectStatement; - - fn table_name(mut self, table_name: DynIden) -> SelectStatement { - self.from(table_name); - self - } -} - -impl QueryTable for sea_query::TableCreateStatement { - type Statement = sea_query::TableCreateStatement; - - fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement { - self.table(table_name); - self - } -} - -impl QueryTable for sea_orm::Insert -where - A: ActiveModelTrait, -{ - type Statement = sea_orm::Insert; - - fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert { - sea_orm::QueryTrait::query(&mut self).into_table(table_name); - self - } -} - -impl QueryTable for sea_orm::DeleteMany -where - E: EntityTrait, -{ - type Statement = sea_orm::DeleteMany; - - fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany { - sea_orm::QueryTrait::query(&mut self).from_table(table_name); - self - } + exec_down_with( + manager, + steps, + M::get_applied_migrations(db).await?, + M::migration_table_name(), + ) + .await } diff --git a/sea-orm-migration/src/migrator/exec.rs b/sea-orm-migration/src/migrator/exec.rs new file mode 100644 index 0000000000..5650d1fa13 --- /dev/null +++ b/sea-orm-migration/src/migrator/exec.rs @@ -0,0 +1,333 @@ +use std::collections::HashSet; +#[cfg(not(feature = "with-time"))] +use std::time::SystemTime; +use tracing::info; + +use super::{Migration, MigrationStatus, queries::*}; +use crate::{SchemaManager, seaql_migrations}; +use sea_orm::sea_query::{ + Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, Table, extension::postgres::Type, +}; +use sea_orm::{ + ActiveValue, ConnectionTrait, DbBackend, DbErr, DynIden, EntityTrait, FromQueryResult, + Iterable, QueryFilter, Schema, Statement, TransactionTrait, +}; + +pub async fn get_migration_models( + db: &C, + migration_table_name: DynIden, +) -> Result, DbErr> +where + C: ConnectionTrait, +{ + let stmt = Query::select() + .table_name(migration_table_name) + .columns(seaql_migrations::Column::iter().map(IntoIden::into_iden)) + .order_by(seaql_migrations::Column::Version, Order::Asc) + .take(); + + db.query_all(&stmt) + .await? + .into_iter() + .map(|row| seaql_migrations::Model::from_query_result(&row, "")) + .collect() +} + +pub fn get_migration_with_status( + migration_files: Vec, + migration_models: Vec, +) -> Result, DbErr> { + let mut migration_files = migration_files; + + let migration_in_db: HashSet = migration_models + .into_iter() + .map(|model| model.version) + .collect(); + let migration_in_fs: HashSet = migration_files + .iter() + .map(|file| file.migration.name().to_string()) + .collect(); + + let pending_migrations = &migration_in_fs - &migration_in_db; + for migration_file in migration_files.iter_mut() { + if !pending_migrations.contains(migration_file.migration.name()) { + migration_file.status = MigrationStatus::Applied; + } + } + + let missing_migrations_in_fs = &migration_in_db - &migration_in_fs; + let errors: Vec = missing_migrations_in_fs + .iter() + .map(|missing_migration| { + format!("Migration file of version '{missing_migration}' is missing, this migration has been applied but its file is missing") + }).collect(); + + if !errors.is_empty() { + Err(DbErr::Custom(errors.join("\n"))) + } else { + Ok(migration_files) + } +} + +macro_rules! exec_with_connection { + ($db:ident, $fn:expr) => {{ + async { + let db = $db.into_database_executor(); + + match db.get_database_backend() { + DbBackend::Postgres => { + let transaction = db.begin().await?; + let manager = SchemaManager::new(&transaction); + $fn(&manager).await?; + transaction.commit().await + } + DbBackend::MySql | DbBackend::Sqlite => { + let manager = SchemaManager::new(db); + $fn(&manager).await + } + db => Err(DbErr::BackendNotSupported { + db: db.as_str(), + ctx: "exec_with_connection", + }), + } + } + }}; +} + +pub(crate) use exec_with_connection; + +pub async fn install(db: &C, migration_table_name: DynIden) -> Result<(), DbErr> +where + C: ConnectionTrait, +{ + let builder = db.get_database_backend(); + let schema = Schema::new(builder); + let mut stmt = schema + .create_table_from_entity(seaql_migrations::Entity) + .table_name(migration_table_name); + stmt.if_not_exists(); + db.execute(&stmt).await?; + Ok(()) +} + +pub async fn uninstall( + manager: &SchemaManager<'_>, + migration_table_name: DynIden, +) -> Result<(), DbErr> { + let mut stmt = Table::drop(); + stmt.table(migration_table_name).if_exists().cascade(); + manager.drop_table(stmt).await?; + Ok(()) +} + +pub async fn drop_everything(db: &C) -> Result<(), DbErr> { + let db_backend = db.get_database_backend(); + + // Temporarily disable the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Disabling foreign key check"); + db.execute_raw(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = OFF".to_owned(), + )) + .await?; + info!("Foreign key check disabled"); + } + + // Drop all foreign keys + if db_backend == DbBackend::MySql { + info!("Dropping all foreign keys"); + let stmt = query_mysql_foreign_keys(db); + let rows = db.query_all(&stmt).await?; + for row in rows.into_iter() { + let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?; + let table_name: String = row.try_get("", "TABLE_NAME")?; + info!( + "Dropping foreign key '{}' from table '{}'", + constraint_name, table_name + ); + let mut stmt = ForeignKey::drop(); + stmt.table(Alias::new(table_name.as_str())) + .name(constraint_name.as_str()); + db.execute(&stmt).await?; + info!("Foreign key '{}' has been dropped", constraint_name); + } + info!("All foreign keys dropped"); + } + + // Drop all tables + let stmt = query_tables(db)?; + let rows = db.query_all(&stmt).await?; + for row in rows.into_iter() { + let table_name: String = row.try_get("", "table_name")?; + info!("Dropping table '{}'", table_name); + let mut stmt = Table::drop(); + stmt.table(Alias::new(table_name.as_str())) + .if_exists() + .cascade(); + db.execute(&stmt).await?; + info!("Table '{}' has been dropped", table_name); + } + + // Drop all types + if db_backend == DbBackend::Postgres { + info!("Dropping all types"); + let stmt = query_pg_types(db); + let rows = db.query_all(&stmt).await?; + for row in rows { + let type_name: String = row.try_get("", "typname")?; + info!("Dropping type '{}'", type_name); + let mut stmt = Type::drop(); + stmt.name(Alias::new(&type_name)); + db.execute(&stmt).await?; + info!("Type '{}' has been dropped", type_name); + } + } + + // Restore the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Restoring foreign key check"); + db.execute_raw(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = ON".to_owned(), + )) + .await?; + info!("Foreign key check restored"); + } + + Ok(()) +} + +fn should_use_transaction(migration: &dyn crate::MigrationTrait, backend: DbBackend) -> bool { + match migration.use_transaction() { + Some(v) => v, + None => backend == DbBackend::Postgres, + } +} + +async fn insert_migration_record( + db: &C, + name: &str, + migration_table_name: DynIden, +) -> Result<(), DbErr> { + #[cfg(not(feature = "with-time"))] + let applied_at = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!") + .as_secs() as i64; + #[cfg(feature = "with-time")] + let applied_at = sea_orm::prelude::TimeDateTimeWithTimeZone::now_utc().unix_timestamp(); + seaql_migrations::Entity::insert(seaql_migrations::ActiveModel { + version: ActiveValue::Set(name.to_owned()), + applied_at: ActiveValue::Set(applied_at), + }) + .table_name(migration_table_name) + .exec(db) + .await?; + Ok(()) +} + +async fn delete_migration_record( + db: &C, + name: &str, + migration_table_name: DynIden, +) -> Result<(), DbErr> { + seaql_migrations::Entity::delete_many() + .filter(Expr::col(seaql_migrations::Column::Version).eq(name)) + .table_name(migration_table_name) + .exec(db) + .await?; + Ok(()) +} + +pub async fn exec_up_with( + manager: &SchemaManager<'_>, + mut steps: Option, + pending_migrations: Vec, + migration_table_name: DynIden, +) -> Result<(), DbErr> { + let db = manager.get_connection(); + + if let Some(steps) = steps { + info!("Applying {} pending migrations", steps); + } else { + info!("Applying all pending migrations"); + } + if pending_migrations.is_empty() { + info!("No pending migrations"); + } + + for Migration { migration, .. } in pending_migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + + let use_txn = should_use_transaction(migration.as_ref(), db.get_database_backend()); + info!("Applying migration '{}'", migration.name()); + + if use_txn { + let transaction = db.begin().await?; + let txn_manager = SchemaManager::new(&transaction); + migration.up(&txn_manager).await?; + info!("Migration '{}' has been applied", migration.name()); + insert_migration_record(&transaction, migration.name(), migration_table_name.clone()) + .await?; + transaction.commit().await?; + } else { + migration.up(manager).await?; + info!("Migration '{}' has been applied", migration.name()); + insert_migration_record(db, migration.name(), migration_table_name.clone()).await?; + } + } + + Ok(()) +} + +pub async fn exec_down_with( + manager: &SchemaManager<'_>, + mut steps: Option, + applied_migrations: Vec, + migration_table_name: DynIden, +) -> Result<(), DbErr> { + let db = manager.get_connection(); + + if let Some(steps) = steps { + info!("Rolling back {} applied migrations", steps); + } else { + info!("Rolling back all applied migrations"); + } + if applied_migrations.is_empty() { + info!("No applied migrations"); + } + + for Migration { migration, .. } in applied_migrations.into_iter().rev() { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + + let use_txn = should_use_transaction(migration.as_ref(), db.get_database_backend()); + info!("Rolling back migration '{}'", migration.name()); + + if use_txn { + let transaction = db.begin().await?; + let txn_manager = SchemaManager::new(&transaction); + migration.down(&txn_manager).await?; + info!("Migration '{}' has been rolled back", migration.name()); + delete_migration_record(&transaction, migration.name(), migration_table_name.clone()) + .await?; + transaction.commit().await?; + } else { + migration.down(manager).await?; + info!("Migration '{}' has been rolled back", migration.name()); + delete_migration_record(db, migration.name(), migration_table_name.clone()).await?; + } + } + + Ok(()) +} diff --git a/sea-orm-migration/src/migrator/queries.rs b/sea-orm-migration/src/migrator/queries.rs new file mode 100644 index 0000000000..80c332f360 --- /dev/null +++ b/sea-orm-migration/src/migrator/queries.rs @@ -0,0 +1,191 @@ +use sea_orm::sea_query::{self, Expr, ExprTrait, Query, SelectStatement, SimpleExpr}; +use sea_orm::{ + ActiveModelTrait, Condition, ConnectionTrait, DbErr, DeriveIden, DynIden, EntityTrait, +}; +#[allow(unused_imports)] +use sea_schema::probe::SchemaProbe; + +pub fn query_tables(db: &C) -> Result +where + C: ConnectionTrait, +{ + #[allow(unused_imports)] + use sea_orm::DbBackend; + + match db.get_database_backend() { + #[cfg(feature = "sqlx-mysql")] + DbBackend::MySql => Ok(sea_schema::mysql::MySql.query_tables()), + #[cfg(feature = "sqlx-postgres")] + DbBackend::Postgres => Ok(sea_schema::postgres::Postgres.query_tables()), + #[cfg(feature = "sqlx-sqlite")] + DbBackend::Sqlite => Ok(sea_schema::sqlite::Sqlite.query_tables()), + #[allow(unreachable_patterns)] + other => Err(DbErr::BackendNotSupported { + db: other.as_str(), + ctx: "query_tables", + }), + } +} + +// this function is only called after checking db backend, the panic is unreachable +pub fn get_current_schema(db: &C) -> SimpleExpr +where + C: ConnectionTrait, +{ + #[allow(unused_imports)] + use sea_orm::DbBackend; + + match db.get_database_backend() { + #[cfg(feature = "sqlx-mysql")] + DbBackend::MySql => sea_schema::mysql::MySql::get_current_schema(), + #[cfg(feature = "sqlx-postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres::get_current_schema(), + #[cfg(feature = "sqlx-sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite::get_current_schema(), + #[allow(unreachable_patterns)] + other => panic!("{other:?} feature is off"), + } +} + +#[derive(DeriveIden)] +enum InformationSchema { + #[sea_orm(iden = "information_schema")] + Schema, + #[sea_orm(iden = "TABLE_NAME")] + TableName, + #[sea_orm(iden = "CONSTRAINT_NAME")] + ConstraintName, + TableConstraints, + TableSchema, + ConstraintType, +} + +pub fn query_mysql_foreign_keys(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + let mut stmt = Query::select(); + stmt.columns([ + InformationSchema::TableName, + InformationSchema::ConstraintName, + ]) + .from(( + InformationSchema::Schema, + InformationSchema::TableConstraints, + )) + .cond_where( + Condition::all() + .add(get_current_schema(db).equals(( + InformationSchema::TableConstraints, + InformationSchema::TableSchema, + ))) + .add( + Expr::col(( + InformationSchema::TableConstraints, + InformationSchema::ConstraintType, + )) + .eq("FOREIGN KEY"), + ), + ); + stmt +} + +#[derive(DeriveIden)] +enum PgType { + Table, + Oid, + Typname, + Typnamespace, + Typelem, +} + +#[derive(DeriveIden)] +enum PgDepend { + Table, + Objid, + Deptype, + Refclassid, +} + +#[derive(DeriveIden)] +enum PgNamespace { + Table, + Oid, + Nspname, +} + +pub fn query_pg_types(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + Query::select() + .column(PgType::Typname) + .from(PgType::Table) + .left_join( + PgNamespace::Table, + Expr::col((PgNamespace::Table, PgNamespace::Oid)) + .equals((PgType::Table, PgType::Typnamespace)), + ) + .left_join( + PgDepend::Table, + Expr::col((PgDepend::Table, PgDepend::Objid)) + .equals((PgType::Table, PgType::Oid)) + .and( + Expr::col((PgDepend::Table, PgDepend::Refclassid)) + .eq(Expr::cust("'pg_extension'::regclass::oid")), + ) + .and(Expr::col((PgDepend::Table, PgDepend::Deptype)).eq(Expr::cust("'e'"))), + ) + .and_where(get_current_schema(db).equals((PgNamespace::Table, PgNamespace::Nspname))) + .and_where(Expr::col((PgType::Table, PgType::Typelem)).eq(0)) + .and_where(Expr::col((PgDepend::Table, PgDepend::Objid)).is_null()) + .take() +} + +pub trait QueryTable { + type Statement; + + fn table_name(self, table_name: DynIden) -> Self::Statement; +} + +impl QueryTable for SelectStatement { + type Statement = SelectStatement; + + fn table_name(mut self, table_name: DynIden) -> SelectStatement { + self.from(table_name); + self + } +} + +impl QueryTable for sea_query::TableCreateStatement { + type Statement = sea_query::TableCreateStatement; + + fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement { + self.table(table_name); + self + } +} + +impl QueryTable for sea_orm::Insert +where + A: ActiveModelTrait, +{ + type Statement = sea_orm::Insert; + + fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert { + sea_orm::QueryTrait::query(&mut self).into_table(table_name); + self + } +} + +impl QueryTable for sea_orm::DeleteMany +where + E: EntityTrait, +{ + type Statement = sea_orm::DeleteMany; + + fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany { + sea_orm::QueryTrait::query(&mut self).from_table(table_name); + self + } +} diff --git a/sea-orm-migration/src/migrator/with_self.rs b/sea-orm-migration/src/migrator/with_self.rs new file mode 100644 index 0000000000..e247e3b666 --- /dev/null +++ b/sea-orm-migration/src/migrator/with_self.rs @@ -0,0 +1,327 @@ +use super::{Migration, MigrationStatus, exec::*}; +use crate::{IntoSchemaManagerConnection, MigrationTrait, SchemaManager, seaql_migrations}; +use sea_orm::sea_query::IntoIden; +use sea_orm::{ConnectionTrait, DbBackend, DbErr, DynIden, TransactionTrait}; + +use tracing::info; + +/// Performing migrations on a database +#[async_trait::async_trait] +pub trait MigratorTraitSelf: Sized + Send + Sync { + /// Vector of migrations in time sequence + fn migrations(&self) -> Vec>; + + /// Name of the migration table, it is `seaql_migrations` by default + fn migration_table_name(&self) -> DynIden { + seaql_migrations::Entity.into_iden() + } + + /// Get list of migrations wrapped in `Migration` struct + fn get_migration_files(&self) -> Vec { + self.migrations() + .into_iter() + .map(|migration| Migration { + migration, + status: MigrationStatus::Pending, + }) + .collect() + } + + /// Get list of applied migrations from database + async fn get_migration_models(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + self.install(db).await?; + get_migration_models(db, self.migration_table_name()).await + } + + /// Get list of migrations with status + async fn get_migration_with_status(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + self.install(db).await?; + get_migration_with_status( + self.get_migration_files(), + self.get_migration_models(db).await?, + ) + } + + /// Get list of pending migrations + async fn get_pending_migrations(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + self.install(db).await?; + Ok(self + .get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Pending) + .collect()) + } + + /// Get list of applied migrations + async fn get_applied_migrations(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + self.install(db).await?; + Ok(self + .get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Applied) + .collect()) + } + + /// Create migration table `seaql_migrations` in the database + async fn install(&self, db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + install(db, self.migration_table_name()).await + } + + /// Check the status of all migrations + async fn status(&self, db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + self.install(db).await?; + + info!("Checking migration status"); + + for Migration { migration, status } in self.get_migration_with_status(db).await? { + info!("Migration '{}'... {}", migration.name(), status); + } + + Ok(()) + } + + /// Drop all tables from the database, then reapply all migrations + async fn fresh<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection!(db, async |manager| { exec_fresh(self, manager).await }).await + } + + /// Rollback all applied migrations, then reapply all migrations + async fn refresh<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection!(db, async |manager| { + exec_down(self, manager, None).await?; + exec_up(self, manager, None).await + }) + .await + } + + /// Rollback all applied migrations + async fn reset<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection!(db, async |manager| { + // Rollback all applied migrations first + exec_down(self, manager, None).await?; + + // Then drop the migration table itself + uninstall(manager, self.migration_table_name()).await + }) + .await + } + + /// Uninstall migration tracking table only (non-destructive) + /// This will drop the `seaql_migrations` table but won't rollback other schema changes. + async fn uninstall<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection!(db, async |manager| { + uninstall(manager, self.migration_table_name()).await + }) + .await + } + + /// Apply pending migrations + async fn up<'c, C>(&self, db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + let db = db.into_database_executor(); + let manager = SchemaManager::new(db); + exec_up(self, &manager, steps).await + } + + /// Rollback applied migrations + async fn down<'c, C>(&self, db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + let db = db.into_database_executor(); + let manager = SchemaManager::new(db); + exec_down(self, &manager, steps).await + } +} + +#[async_trait::async_trait] +impl MigratorTraitSelf for M +where + M: super::MigratorTrait + Sized + Send + Sync, +{ + fn migrations(&self) -> Vec> { + M::migrations() + } + + fn migration_table_name(&self) -> DynIden { + M::migration_table_name() + } + + fn get_migration_files(&self) -> Vec { + M::get_migration_files() + } + + async fn get_migration_models(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + M::get_migration_models(db).await + } + + async fn get_migration_with_status(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + M::get_migration_with_status(db).await + } + + async fn get_pending_migrations(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + M::get_pending_migrations(db).await + } + + async fn get_applied_migrations(&self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + M::get_applied_migrations(db).await + } + + async fn install(&self, db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + M::install(db).await + } + + /// Check the status of all migrations + async fn status(&self, db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + M::status(db).await + } + + async fn fresh<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + M::fresh(db).await + } + + async fn refresh<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + M::refresh(db).await + } + + async fn reset<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + M::reset(db).await + } + + async fn uninstall<'c, C>(&self, db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + M::uninstall(db).await + } + + async fn up<'c, C>(&self, db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + M::up(db, steps).await + } + + async fn down<'c, C>(&self, db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + M::down(db, steps).await + } +} + +async fn exec_fresh(migrator: &M, manager: &SchemaManager<'_>) -> Result<(), DbErr> +where + M: MigratorTraitSelf, +{ + let db = manager.get_connection(); + + migrator.install(db).await?; + + drop_everything(db).await?; + + exec_up(migrator, manager, None).await +} + +async fn exec_up( + migrator: &M, + manager: &SchemaManager<'_>, + steps: Option, +) -> Result<(), DbErr> +where + M: MigratorTraitSelf, +{ + let db = manager.get_connection(); + + migrator.install(db).await?; + + exec_up_with( + manager, + steps, + migrator.get_pending_migrations(db).await?, + migrator.migration_table_name(), + ) + .await +} + +async fn exec_down( + migrator: &M, + manager: &SchemaManager<'_>, + steps: Option, +) -> Result<(), DbErr> +where + M: MigratorTraitSelf, +{ + let db = manager.get_connection(); + + migrator.install(db).await?; + + exec_down_with( + manager, + steps, + migrator.get_applied_migrations(db).await?, + migrator.migration_table_name(), + ) + .await +} diff --git a/sea-orm-migration/src/prelude.rs b/sea-orm-migration/src/prelude.rs index 9b1030ff3a..4868cfe3c8 100644 --- a/sea-orm-migration/src/prelude.rs +++ b/sea-orm-migration/src/prelude.rs @@ -7,7 +7,6 @@ pub use crate::{ }; pub use async_trait; pub use sea_orm::{ - self, + self, ConnectionTrait, DbErr, DeriveIden, DeriveMigrationName, sea_query::{self, *}, - ConnectionTrait, DbErr, DeriveMigrationName, }; diff --git a/sea-orm-migration/src/schema.rs b/sea-orm-migration/src/schema.rs new file mode 100644 index 0000000000..903ad76c39 --- /dev/null +++ b/sea-orm-migration/src/schema.rs @@ -0,0 +1,606 @@ +//! > Adapted from https://github.com/loco-rs/loco/blob/master/src/schema.rs +//! +//! # Database Table Schema Helpers +//! +//! This module defines functions and helpers for creating database table +//! schemas using the `sea-orm` and `sea-query` libraries. +//! +//! # Example +//! +//! The following example shows how the user migration file should be and using +//! the schema helpers to create the Db fields. +//! +//! ```rust +//! use sea_orm_migration::{prelude::*, schema::*}; +//! +//! #[derive(DeriveMigrationName)] +//! pub struct Migration; +//! +//! #[async_trait::async_trait] +//! impl MigrationTrait for Migration { +//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! let table = table_auto("users") +//! .col(pk_auto("id")) +//! .col(uuid("pid")) +//! .col(string_uniq("email")) +//! .col(string("password")) +//! .col(string("name")) +//! .col(string_null("reset_token")) +//! .col(timestamp_null("reset_sent_at")) +//! .to_owned(); +//! manager.create_table(table).await?; +//! Ok(()) +//! } +//! +//! async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! manager +//! .drop_table(Table::drop().table("users").to_owned()) +//! .await +//! } +//! } +//! ``` + +use crate::{prelude::Iden, sea_query}; +use sea_orm::sea_query::{ + Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement, +}; + +#[derive(Iden)] +enum GeneralIds { + CreatedAt, + UpdatedAt, +} + +/// Create a table with `created_at` and `updated_at` added by default +pub fn table_auto(name: T) -> TableCreateStatement { + timestamps(Table::create().table(name).if_not_exists().take()) +} + +/// Create a primary key column with auto-increment +pub fn pk_auto(name: T) -> ColumnDef { + integer(name).auto_increment().primary_key().take() +} + +/// Create a primary key column of big integer with auto-increment +pub fn big_pk_auto(name: T) -> ColumnDef { + big_integer(name).auto_increment().primary_key().take() +} + +/// Create a UUID primary key +pub fn pk_uuid(name: T) -> ColumnDef { + uuid(name).primary_key().take() +} + +pub fn char_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).char_len(length).not_null().take() +} + +pub fn char_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).char_len(length).null().take() +} + +pub fn char_len_uniq(col: T, length: u32) -> ColumnDef { + char_len(col, length).unique_key().take() +} + +pub fn char(col: T) -> ColumnDef { + ColumnDef::new(col).char().not_null().take() +} + +pub fn char_null(col: T) -> ColumnDef { + ColumnDef::new(col).char().null().take() +} + +pub fn char_uniq(col: T) -> ColumnDef { + char(col).unique_key().take() +} + +pub fn string_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).string_len(length).not_null().take() +} + +pub fn string_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).string_len(length).null().take() +} + +pub fn string_len_uniq(col: T, length: u32) -> ColumnDef { + string_len(col, length).unique_key().take() +} + +pub fn string(col: T) -> ColumnDef { + ColumnDef::new(col).string().not_null().take() +} + +pub fn string_null(col: T) -> ColumnDef { + ColumnDef::new(col).string().null().take() +} + +pub fn string_uniq(col: T) -> ColumnDef { + string(col).unique_key().take() +} + +pub fn text(col: T) -> ColumnDef { + ColumnDef::new(col).text().not_null().take() +} + +pub fn text_null(col: T) -> ColumnDef { + ColumnDef::new(col).text().null().take() +} + +pub fn text_uniq(col: T) -> ColumnDef { + text(col).unique_key().take() +} + +pub fn tiny_integer(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_integer().not_null().take() +} + +pub fn tiny_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_integer().null().take() +} + +pub fn tiny_integer_uniq(col: T) -> ColumnDef { + tiny_integer(col).unique_key().take() +} + +pub fn small_integer(col: T) -> ColumnDef { + ColumnDef::new(col).small_integer().not_null().take() +} + +pub fn small_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).small_integer().null().take() +} + +pub fn small_integer_uniq(col: T) -> ColumnDef { + small_integer(col).unique_key().take() +} + +pub fn integer(col: T) -> ColumnDef { + ColumnDef::new(col).integer().not_null().take() +} + +pub fn integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).integer().null().take() +} + +pub fn integer_uniq(col: T) -> ColumnDef { + integer(col).unique_key().take() +} + +pub fn big_integer(col: T) -> ColumnDef { + ColumnDef::new(col).big_integer().not_null().take() +} + +pub fn big_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).big_integer().null().take() +} + +pub fn big_integer_uniq(col: T) -> ColumnDef { + big_integer(col).unique_key().take() +} + +pub fn tiny_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_unsigned().not_null().take() +} + +pub fn tiny_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_unsigned().null().take() +} + +pub fn tiny_unsigned_uniq(col: T) -> ColumnDef { + tiny_unsigned(col).unique_key().take() +} + +pub fn small_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).small_unsigned().not_null().take() +} + +pub fn small_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).small_unsigned().null().take() +} + +pub fn small_unsigned_uniq(col: T) -> ColumnDef { + small_unsigned(col).unique_key().take() +} + +pub fn unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).unsigned().not_null().take() +} + +pub fn unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).unsigned().null().take() +} + +pub fn unsigned_uniq(col: T) -> ColumnDef { + unsigned(col).unique_key().take() +} + +pub fn big_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).big_unsigned().not_null().take() +} + +pub fn big_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).big_unsigned().null().take() +} + +pub fn big_unsigned_uniq(col: T) -> ColumnDef { + big_unsigned(col).unique_key().take() +} + +pub fn float(col: T) -> ColumnDef { + ColumnDef::new(col).float().not_null().take() +} + +pub fn float_null(col: T) -> ColumnDef { + ColumnDef::new(col).float().null().take() +} + +pub fn float_uniq(col: T) -> ColumnDef { + float(col).unique_key().take() +} + +pub fn double(col: T) -> ColumnDef { + ColumnDef::new(col).double().not_null().take() +} + +pub fn double_null(col: T) -> ColumnDef { + ColumnDef::new(col).double().null().take() +} + +pub fn double_uniq(col: T) -> ColumnDef { + double(col).unique_key().take() +} + +pub fn decimal_len(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .decimal_len(precision, scale) + .not_null() + .take() +} + +pub fn decimal_len_null(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .decimal_len(precision, scale) + .null() + .take() +} + +pub fn decimal_len_uniq(col: T, precision: u32, scale: u32) -> ColumnDef { + decimal_len(col, precision, scale).unique_key().take() +} + +pub fn decimal(col: T) -> ColumnDef { + ColumnDef::new(col).decimal().not_null().take() +} + +pub fn decimal_null(col: T) -> ColumnDef { + ColumnDef::new(col).decimal().null().take() +} + +pub fn decimal_uniq(col: T) -> ColumnDef { + decimal(col).unique_key().take() +} + +pub fn date_time(col: T) -> ColumnDef { + ColumnDef::new(col).date_time().not_null().take() +} + +pub fn date_time_null(col: T) -> ColumnDef { + ColumnDef::new(col).date_time().null().take() +} + +pub fn date_time_uniq(col: T) -> ColumnDef { + date_time(col).unique_key().take() +} + +pub fn interval( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + ColumnDef::new(col) + .interval(fields, precision) + .not_null() + .take() +} + +pub fn interval_null( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + ColumnDef::new(col) + .interval(fields, precision) + .null() + .take() +} + +pub fn interval_uniq( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + interval(col, fields, precision).unique_key().take() +} + +pub fn timestamp(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp().not_null().take() +} + +pub fn timestamp_null(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp().null().take() +} + +pub fn timestamp_uniq(col: T) -> ColumnDef { + timestamp(col).unique_key().take() +} + +pub fn timestamp_with_time_zone(col: T) -> ColumnDef { + ColumnDef::new(col) + .timestamp_with_time_zone() + .not_null() + .take() +} + +pub fn timestamp_with_time_zone_null(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp_with_time_zone().null().take() +} + +pub fn timestamp_with_time_zone_uniq(col: T) -> ColumnDef { + timestamp_with_time_zone(col).unique_key().take() +} + +pub fn time(col: T) -> ColumnDef { + ColumnDef::new(col).time().not_null().take() +} + +pub fn time_null(col: T) -> ColumnDef { + ColumnDef::new(col).time().null().take() +} + +pub fn time_uniq(col: T) -> ColumnDef { + time(col).unique_key().take() +} + +pub fn date(col: T) -> ColumnDef { + ColumnDef::new(col).date().not_null().take() +} + +pub fn date_null(col: T) -> ColumnDef { + ColumnDef::new(col).date().null().take() +} + +pub fn date_uniq(col: T) -> ColumnDef { + date(col).unique_key().take() +} + +pub fn year(col: T) -> ColumnDef { + ColumnDef::new(col).year().not_null().take() +} + +pub fn year_null(col: T) -> ColumnDef { + ColumnDef::new(col).year().null().take() +} + +pub fn year_uniq(col: T) -> ColumnDef { + year(col).unique_key().take() +} + +pub fn binary_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).binary_len(length).not_null().take() +} + +pub fn binary_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).binary_len(length).null().take() +} + +pub fn binary_len_uniq(col: T, length: u32) -> ColumnDef { + binary_len(col, length).unique_key().take() +} + +pub fn binary(col: T) -> ColumnDef { + ColumnDef::new(col).binary().not_null().take() +} + +pub fn binary_null(col: T) -> ColumnDef { + ColumnDef::new(col).binary().null().take() +} + +pub fn binary_uniq(col: T) -> ColumnDef { + binary(col).unique_key().take() +} + +pub fn var_binary(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).var_binary(length).not_null().take() +} + +pub fn var_binary_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).var_binary(length).null().take() +} + +pub fn var_binary_uniq(col: T, length: u32) -> ColumnDef { + var_binary(col, length).unique_key().take() +} + +pub fn bit(col: T, length: Option) -> ColumnDef { + ColumnDef::new(col).bit(length).not_null().take() +} + +pub fn bit_null(col: T, length: Option) -> ColumnDef { + ColumnDef::new(col).bit(length).null().take() +} + +pub fn bit_uniq(col: T, length: Option) -> ColumnDef { + bit(col, length).unique_key().take() +} + +pub fn varbit(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).varbit(length).not_null().take() +} + +pub fn varbit_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).varbit(length).null().take() +} + +pub fn varbit_uniq(col: T, length: u32) -> ColumnDef { + varbit(col, length).unique_key().take() +} + +pub fn blob(col: T) -> ColumnDef { + ColumnDef::new(col).blob().not_null().take() +} + +pub fn blob_null(col: T) -> ColumnDef { + ColumnDef::new(col).blob().null().take() +} + +pub fn blob_uniq(col: T) -> ColumnDef { + blob(col).unique_key().take() +} + +pub fn boolean(col: T) -> ColumnDef { + ColumnDef::new(col).boolean().not_null().take() +} + +pub fn boolean_null(col: T) -> ColumnDef { + ColumnDef::new(col).boolean().null().take() +} + +pub fn boolean_uniq(col: T) -> ColumnDef { + boolean(col).unique_key().take() +} + +pub fn money_len(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .money_len(precision, scale) + .not_null() + .take() +} + +pub fn money_len_null(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .money_len(precision, scale) + .null() + .take() +} + +pub fn money_len_uniq(col: T, precision: u32, scale: u32) -> ColumnDef { + money_len(col, precision, scale).unique_key().take() +} + +pub fn money(col: T) -> ColumnDef { + ColumnDef::new(col).money().not_null().take() +} + +pub fn money_null(col: T) -> ColumnDef { + ColumnDef::new(col).money().null().take() +} + +pub fn money_uniq(col: T) -> ColumnDef { + money(col).unique_key().take() +} + +pub fn json(col: T) -> ColumnDef { + ColumnDef::new(col).json().not_null().take() +} + +pub fn json_null(col: T) -> ColumnDef { + ColumnDef::new(col).json().null().take() +} + +pub fn json_uniq(col: T) -> ColumnDef { + json(col).unique_key().take() +} + +pub fn json_binary(col: T) -> ColumnDef { + ColumnDef::new(col).json_binary().not_null().take() +} + +pub fn json_binary_null(col: T) -> ColumnDef { + ColumnDef::new(col).json_binary().null().take() +} + +pub fn json_binary_uniq(col: T) -> ColumnDef { + json_binary(col).unique_key().take() +} + +pub fn uuid(col: T) -> ColumnDef { + ColumnDef::new(col).uuid().not_null().take() +} + +pub fn uuid_null(col: T) -> ColumnDef { + ColumnDef::new(col).uuid().null().take() +} + +pub fn uuid_uniq(col: T) -> ColumnDef { + uuid(col).unique_key().take() +} + +pub fn custom(col: T, name: N) -> ColumnDef { + ColumnDef::new(col).custom(name).not_null().take() +} + +pub fn custom_null(col: T, name: N) -> ColumnDef { + ColumnDef::new(col).custom(name).null().take() +} + +pub fn enumeration(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + ColumnDef::new(col) + .enumeration(name, variants) + .not_null() + .take() +} + +pub fn enumeration_null(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + ColumnDef::new(col) + .enumeration(name, variants) + .null() + .take() +} + +pub fn enumeration_uniq(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + enumeration(col, name, variants).unique_key().take() +} + +pub fn array(col: T, elem_type: ColumnType) -> ColumnDef { + ColumnDef::new(col).array(elem_type).not_null().take() +} + +pub fn array_null(col: T, elem_type: ColumnType) -> ColumnDef { + ColumnDef::new(col).array(elem_type).null().take() +} + +pub fn array_uniq(col: T, elem_type: ColumnType) -> ColumnDef { + array(col, elem_type).unique_key().take() +} + +/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. +pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { + let mut t = t; + t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) + .col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp())) + .take() +} + +/// Create an Alias. +pub fn name>(name: T) -> Alias { + Alias::new(name) +} diff --git a/sea-orm-migration/tests/common/migration/m20220118_000001_create_cake_table.rs b/sea-orm-migration/tests/common/migration/m20220118_000001_create_cake_table.rs index f06513aa62..4ab98928a8 100644 --- a/sea-orm-migration/tests/common/migration/m20220118_000001_create_cake_table.rs +++ b/sea-orm-migration/tests/common/migration/m20220118_000001_create_cake_table.rs @@ -1,4 +1,4 @@ -use sea_orm_migration::prelude::*; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -9,23 +9,28 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Cake::Table) - .col( - ColumnDef::new(Cake::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Cake::Name).string().not_null()) + .table("cake") + .col(pk_auto("id")) + .col(string("name")) .to_owned(), ) - .await + .await?; + + manager + .create_index( + Index::create() + .name("cake_name_index") + .table("cake") + .col("name") + .to_owned(), + ) + .await?; + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager - .drop_table(Table::drop().table(Cake::Table).to_owned()) + .drop_table(Table::drop().table("cake").to_owned()) .await?; if std::env::var_os("ABORT_MIGRATION").eq(&Some("YES".into())) { @@ -37,11 +42,3 @@ impl MigrationTrait for Migration { Ok(()) } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -pub enum Cake { - Table, - Id, - Name, -} diff --git a/sea-orm-migration/tests/common/migration/m20220118_000002_create_fruit_table.rs b/sea-orm-migration/tests/common/migration/m20220118_000002_create_fruit_table.rs index 07ead83499..653c872fcf 100644 --- a/sea-orm-migration/tests/common/migration/m20220118_000002_create_fruit_table.rs +++ b/sea-orm-migration/tests/common/migration/m20220118_000002_create_fruit_table.rs @@ -1,6 +1,5 @@ -use super::m20220118_000001_create_cake_table::Cake; -use sea_orm_migration::prelude::*; use sea_orm_migration::sea_orm::DbBackend; +use sea_orm_migration::{prelude::*, schema::*}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -11,21 +10,15 @@ impl MigrationTrait for Migration { manager .create_table( Table::create() - .table(Fruit::Table) - .col( - ColumnDef::new(Fruit::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(Fruit::Name).string().not_null()) - .col(ColumnDef::new(Fruit::CakeId).integer().not_null()) + .table("fruit") + .col(pk_auto("id")) + .col(string("name")) + .col(integer("cake_id")) .foreign_key( ForeignKey::create() .name("fk-fruit-cake_id") - .from(Fruit::Table, Fruit::CakeId) - .to(Cake::Table, Cake::Id), + .from("fruit", "cake_id") + .to("cake", "id"), ) .to_owned(), ) @@ -37,23 +30,14 @@ impl MigrationTrait for Migration { manager .drop_foreign_key( ForeignKey::drop() - .table(Fruit::Table) + .table("fruit") .name("fk-fruit-cake_id") .to_owned(), ) .await?; } manager - .drop_table(Table::drop().table(Fruit::Table).to_owned()) + .drop_table(Table::drop().table("fruit").to_owned()) .await } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -pub enum Fruit { - Table, - Id, - Name, - CakeId, -} diff --git a/sea-orm-migration/tests/common/migration/m20220118_000004_create_tea_enum.rs b/sea-orm-migration/tests/common/migration/m20220118_000004_create_tea_enum.rs index ba9b68e6b1..d8beca9407 100644 --- a/sea-orm-migration/tests/common/migration/m20220118_000004_create_tea_enum.rs +++ b/sea-orm-migration/tests/common/migration/m20220118_000004_create_tea_enum.rs @@ -9,18 +9,15 @@ impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); - match db.get_database_backend() { - DbBackend::MySql | DbBackend::Sqlite => {} - DbBackend::Postgres => { - manager - .create_type( - Type::create() - .as_enum(Tea::Table) - .values([Tea::EverydayTea, Tea::BreakfastTea]) - .to_owned(), - ) - .await?; - } + if db.get_database_backend() == DbBackend::Postgres { + manager + .create_type( + Type::create() + .as_enum(Tea::Enum) + .values([Tea::EverydayTea, Tea::BreakfastTea]) + .to_owned(), + ) + .await?; } Ok(()) @@ -29,25 +26,22 @@ impl MigrationTrait for Migration { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); - match db.get_database_backend() { - DbBackend::MySql | DbBackend::Sqlite => {} - DbBackend::Postgres => { - manager - .drop_type(Type::drop().name(Tea::Table).to_owned()) - .await?; - } + if db.get_database_backend() == DbBackend::Postgres { + manager + .drop_type(Type::drop().name(Tea::Enum).to_owned()) + .await?; } Ok(()) } } -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] +#[derive(DeriveIden)] pub enum Tea { - Table, - #[iden = "EverydayTea"] + #[sea_orm(iden = "tea")] + Enum, + #[sea_orm(iden = "EverydayTea")] EverydayTea, - #[iden = "BreakfastTea"] + #[sea_orm(iden = "BreakfastTea")] BreakfastTea, } diff --git a/sea-orm-migration/tests/common/migration/m20220923_000001_seed_cake_table.rs b/sea-orm-migration/tests/common/migration/m20220923_000001_seed_cake_table.rs index 16c019bf82..f35cfec6bb 100644 --- a/sea-orm-migration/tests/common/migration/m20220923_000001_seed_cake_table.rs +++ b/sea-orm-migration/tests/common/migration/m20220923_000001_seed_cake_table.rs @@ -7,31 +7,24 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let insert = Query::insert() - .into_table(Cake::Table) - .columns([Cake::Name]) + .into_table("cake") + .columns(["name"]) .values_panic(["Tiramisu".into()]) .to_owned(); - manager.exec_stmt(insert).await?; + manager.execute(insert).await?; Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { let delete = Query::delete() - .from_table(Cake::Table) - .and_where(Expr::col(Cake::Name).eq("Tiramisu")) + .from_table("cake") + .and_where(Expr::col("name").eq("Tiramisu")) .to_owned(); - manager.exec_stmt(delete).await?; + manager.execute(delete).await?; Ok(()) } } - -/// Learn more at https://docs.rs/sea-query#iden -#[derive(Iden)] -pub enum Cake { - Table, - Name, -} diff --git a/sea-orm-migration/tests/common/migration/m20250101_000001_create_test_table.rs b/sea-orm-migration/tests/common/migration/m20250101_000001_create_test_table.rs new file mode 100644 index 0000000000..6e6085d645 --- /dev/null +++ b/sea-orm-migration/tests/common/migration/m20250101_000001_create_test_table.rs @@ -0,0 +1,63 @@ +use sea_orm_migration::prelude::*; +use sea_orm_migration::schema::*; +use sea_orm_migration::sea_orm::DbBackend; + +pub struct Migration { + pub use_transaction: Option, + pub should_fail: bool, +} + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20250101_000001_create_test_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + fn use_transaction(&self) -> Option { + self.use_transaction + } + + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let expect_txn = self + .use_transaction + .unwrap_or(manager.get_database_backend() == DbBackend::Postgres); + assert_eq!( + manager.get_connection().is_transaction(), + expect_txn, + "up: expected is_transaction() = {expect_txn}" + ); + + manager + .create_table( + Table::create() + .table("test_table") + .col(pk_auto("id")) + .col(string("name")) + .to_owned(), + ) + .await?; + + if self.should_fail { + return Err(DbErr::Migration("intentional failure".into())); + } + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let expect_txn = self + .use_transaction + .unwrap_or(manager.get_database_backend() == DbBackend::Postgres); + assert_eq!( + manager.get_connection().is_transaction(), + expect_txn, + "down: expected is_transaction() = {expect_txn}" + ); + + manager + .drop_table(Table::drop().table("test_table").to_owned()) + .await + } +} diff --git a/sea-orm-migration/tests/common/migration/m20250101_000002_manual_transaction.rs b/sea-orm-migration/tests/common/migration/m20250101_000002_manual_transaction.rs new file mode 100644 index 0000000000..f9eac8766c --- /dev/null +++ b/sea-orm-migration/tests/common/migration/m20250101_000002_manual_transaction.rs @@ -0,0 +1,50 @@ +use sea_orm_migration::prelude::*; +use sea_orm_migration::schema::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20250101_000002_manual_transaction" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + fn use_transaction(&self) -> Option { + Some(false) + } + + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + assert!( + !manager.get_connection().is_transaction(), + "outer manager should not be in a transaction" + ); + + let m = manager.begin().await?; + assert!( + m.get_connection().is_transaction(), + "inner manager should be in a transaction" + ); + m.create_table( + Table::create() + .table("manual_txn_table") + .col(pk_auto("id")) + .col(string("name")) + .to_owned(), + ) + .await?; + m.commit().await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let m = manager.begin().await?; + m.drop_table(Table::drop().table("manual_txn_table").to_owned()) + .await?; + m.commit().await?; + + Ok(()) + } +} diff --git a/sea-orm-migration/tests/common/migration/mod.rs b/sea-orm-migration/tests/common/migration/mod.rs index 4e85a13ecb..350a01e19a 100644 --- a/sea-orm-migration/tests/common/migration/mod.rs +++ b/sea-orm-migration/tests/common/migration/mod.rs @@ -4,3 +4,5 @@ pub mod m20220118_000003_seed_cake_table; pub mod m20220118_000004_create_tea_enum; pub mod m20220923_000001_seed_cake_table; pub mod m20230109_000001_seed_cake_table; +pub mod m20250101_000001_create_test_table; +pub mod m20250101_000002_manual_transaction; diff --git a/sea-orm-migration/tests/common/migrator/mod.rs b/sea-orm-migration/tests/common/migrator/mod.rs index 772da4927d..641ba5b502 100644 --- a/sea-orm-migration/tests/common/migrator/mod.rs +++ b/sea-orm-migration/tests/common/migrator/mod.rs @@ -1,2 +1,4 @@ pub mod default; pub mod override_migration_table_name; +pub mod transaction_test; +pub mod with_self; diff --git a/sea-orm-migration/tests/common/migrator/override_migration_table_name.rs b/sea-orm-migration/tests/common/migrator/override_migration_table_name.rs index 745122a9b5..9a22868585 100644 --- a/sea-orm-migration/tests/common/migrator/override_migration_table_name.rs +++ b/sea-orm-migration/tests/common/migrator/override_migration_table_name.rs @@ -17,6 +17,6 @@ impl MigratorTrait for Migrator { } fn migration_table_name() -> sea_orm::DynIden { - Alias::new("override_migration_table_name").into_iden() + "override_migration_table_name".into_iden() } } diff --git a/sea-orm-migration/tests/common/migrator/transaction_test.rs b/sea-orm-migration/tests/common/migrator/transaction_test.rs new file mode 100644 index 0000000000..68727df907 --- /dev/null +++ b/sea-orm-migration/tests/common/migrator/transaction_test.rs @@ -0,0 +1,26 @@ +use crate::common::migration::*; +use sea_orm_migration::{MigratorTraitSelf, prelude::*}; + +pub struct Migrator { + pub use_transaction: Option, + pub should_fail: bool, +} + +#[async_trait::async_trait] +impl MigratorTraitSelf for Migrator { + fn migrations(&self) -> Vec> { + vec![Box::new(m20250101_000001_create_test_table::Migration { + use_transaction: self.use_transaction, + should_fail: self.should_fail, + })] + } +} + +pub struct ManualTxnMigrator; + +#[async_trait::async_trait] +impl MigratorTrait for ManualTxnMigrator { + fn migrations() -> Vec> { + vec![Box::new(m20250101_000002_manual_transaction::Migration)] + } +} diff --git a/sea-orm-migration/tests/common/migrator/with_self.rs b/sea-orm-migration/tests/common/migrator/with_self.rs new file mode 100644 index 0000000000..5734817fe2 --- /dev/null +++ b/sea-orm-migration/tests/common/migrator/with_self.rs @@ -0,0 +1,20 @@ +use crate::common::migration::*; +use sea_orm_migration::{MigratorTraitSelf, prelude::*}; + +pub struct Migrator { + pub i: i32, +} + +#[async_trait::async_trait] +impl MigratorTraitSelf for Migrator { + fn migrations(&self) -> Vec> { + vec![ + Box::new(m20220118_000001_create_cake_table::Migration), + Box::new(m20220118_000002_create_fruit_table::Migration), + Box::new(m20220118_000003_seed_cake_table::Migration), + Box::new(m20220118_000004_create_tea_enum::Migration), + Box::new(m20220923_000001_seed_cake_table::Migration), + Box::new(m20230109_000001_seed_cake_table::Migration), + ] + } +} diff --git a/sea-orm-migration/tests/main.rs b/sea-orm-migration/tests/main.rs index 3247bae25f..9d1a0240ab 100644 --- a/sea-orm-migration/tests/main.rs +++ b/sea-orm-migration/tests/main.rs @@ -2,9 +2,9 @@ mod common; use common::migrator::*; use sea_orm::{ConnectOptions, ConnectionTrait, Database, DbBackend, DbErr, Statement}; -use sea_orm_migration::{migrator::MigrationStatus, prelude::*}; +use sea_orm_migration::{MigratorTraitSelf, migrator::MigrationStatus, prelude::*}; -#[async_std::test] +#[tokio::test] async fn main() -> Result<(), DbErr> { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) @@ -22,6 +22,14 @@ async fn main() -> Result<(), DbErr> { ) .await?; + run_migration( + url, + with_self::Migrator { i: 12 }, + "sea_orm_migration_self", + "public", + ) + .await?; + run_migration( url, override_migration_table_name::Migrator, @@ -37,21 +45,19 @@ async fn main() -> Result<(), DbErr> { ) .await?; + run_transaction_test(url, "sea_orm_migration_txn", "public").await?; + Ok(()) } -async fn run_migration( +async fn create_db( url: &str, - _: Migrator, db_name: &str, schema: &str, -) -> Result<(), DbErr> -where - Migrator: MigratorTrait, -{ +) -> Result { let db_connect = |url: String| async { let connect_options = ConnectOptions::new(url) - .set_schema_search_path(schema.to_owned()) + .set_schema_search_path(format!("{schema},public")) .to_owned(); Database::connect(connect_options).await @@ -59,24 +65,24 @@ where let db = db_connect(url.to_owned()).await?; - let db = &match db.get_database_backend() { + match db.get_database_backend() { DbBackend::MySql => { - db.execute(Statement::from_string( + db.execute_raw(Statement::from_string( db.get_database_backend(), format!("CREATE DATABASE IF NOT EXISTS `{db_name}`;"), )) .await?; let url = format!("{url}/{db_name}"); - db_connect(url).await? + db_connect(url).await } DbBackend::Postgres => { - db.execute(Statement::from_string( + db.execute_raw(Statement::from_string( db.get_database_backend(), format!("DROP DATABASE IF EXISTS \"{db_name}\";"), )) .await?; - db.execute(Statement::from_string( + db.execute_raw(Statement::from_string( db.get_database_backend(), format!("CREATE DATABASE \"{db_name}\";"), )) @@ -85,25 +91,36 @@ where let url = format!("{url}/{db_name}"); let db = db_connect(url).await?; - db.execute(Statement::from_string( + db.execute_raw(Statement::from_string( db.get_database_backend(), format!("CREATE SCHEMA IF NOT EXISTS \"{schema}\";"), )) .await?; - db + Ok(db) } - DbBackend::Sqlite => db, - }; + DbBackend::Sqlite => Ok(db), + db => Err(DbErr::BackendNotSupported { + db: db.as_str(), + ctx: "create_db", + }), + } +} + +async fn run_migration(url: &str, migrator: M, db_name: &str, schema: &str) -> Result<(), DbErr> +where + M: MigratorTraitSelf, +{ + let db = &create_db(url, db_name, schema).await?; let manager = SchemaManager::new(db); println!("\nMigrator::status"); - Migrator::status(db).await?; + migrator.status(db).await?; println!("\nMigrator::install"); - Migrator::install(db).await?; + migrator.install(db).await?; - let migration_table_name = Migrator::migration_table_name().to_string(); + let migration_table_name = migrator.migration_table_name().to_string(); let migration_table_name = migration_table_name.as_str(); assert!(manager.has_table(migration_table_name).await?); if migration_table_name != "seaql_migrations" { @@ -111,22 +128,22 @@ where } println!("\nMigrator::reset"); - Migrator::reset(db).await?; + migrator.reset(db).await?; assert!(!manager.has_table("cake").await?); assert!(!manager.has_table("fruit").await?); println!("\nMigrator::up"); - Migrator::up(db, Some(0)).await?; + migrator.up(db, Some(0)).await?; assert!(!manager.has_table("cake").await?); assert!(!manager.has_table("fruit").await?); println!("\nMigrator::up"); - Migrator::up(db, Some(1)).await?; + migrator.up(db, Some(1)).await?; println!("\nMigrator::get_pending_migrations"); - let migrations = Migrator::get_pending_migrations(db).await?; + let migrations = migrator.get_pending_migrations(db).await?; assert_eq!(migrations.len(), 5); let migration = migrations.get(0).unwrap(); @@ -137,57 +154,67 @@ where assert!(!manager.has_table("fruit").await?); println!("\nMigrator::down"); - Migrator::down(db, Some(0)).await?; + migrator.down(db, Some(0)).await?; assert!(manager.has_table("cake").await?); assert!(!manager.has_table("fruit").await?); println!("\nMigrator::down"); - Migrator::down(db, Some(1)).await?; + migrator.down(db, Some(1)).await?; assert!(!manager.has_table("cake").await?); assert!(!manager.has_table("fruit").await?); - // Tests rolling back changes of "migrate up" when running migration on Postgres + // Tests rolling back a failing migration on Postgres. + // With per-migration transactions, only the failing migration is rolled back; + // earlier migrations that committed successfully are preserved. if matches!(db.get_database_backend(), DbBackend::Postgres) { println!("\nRoll back changes when encounter errors"); // Set a flag to throw error inside `m20230109_000001_seed_cake_table.rs` - std::env::set_var("ABORT_MIGRATION", "YES"); + unsafe { + std::env::set_var("ABORT_MIGRATION", "YES"); + } // Should throw an error println!("\nMigrator::up"); assert_eq!( - Migrator::up(db, None).await, + migrator.up(db, None).await, Err(DbErr::Migration( "Abort migration and rollback changes".into() )) ); println!("\nMigrator::status"); - Migrator::status(db).await?; + migrator.status(db).await?; - // Check migrations have been rolled back - assert!(!manager.has_table("cake").await?); - assert!(!manager.has_table("fruit").await?); + // Only the failing migration (m20230109) is rolled back; + // earlier migrations (cake, fruit, etc.) committed successfully + assert!(manager.has_table("cake").await?); + assert!(manager.has_table("fruit").await?); // Unset the flag - std::env::remove_var("ABORT_MIGRATION"); + unsafe { + std::env::remove_var("ABORT_MIGRATION"); + } } println!("\nMigrator::up"); - Migrator::up(db, None).await?; + migrator.up(db, None).await?; println!("\nMigrator::get_applied_migrations"); - let migrations = Migrator::get_applied_migrations(db).await?; + let migrations = migrator.get_applied_migrations(db).await?; assert_eq!(migrations.len(), 6); + assert!(!manager.has_index("cake", "non_existent_index").await?); + assert!(manager.has_index("cake", "cake_name_index").await?); + let migration = migrations.get(0).unwrap(); assert_eq!(migration.name(), "m20220118_000001_create_cake_table"); assert_eq!(migration.status(), MigrationStatus::Applied); println!("\nMigrator::status"); - Migrator::status(db).await?; + migrator.status(db).await?; assert!(manager.has_table("cake").await?); assert!(manager.has_table("fruit").await?); @@ -195,35 +222,44 @@ where assert!(manager.has_column("cake", "name").await?); assert!(manager.has_column("fruit", "cake_id").await?); - // Tests rolling back changes of "migrate down" when running migration on Postgres + // Tests rolling back a failing migration-down on Postgres. + // With per-migration transactions, rollbacks happen one at a time in reverse. + // Migrations 6-2 roll back and commit successfully. Migration 1 (drops cake + // then ABORTs) fails, so its DROP is restored. But migration 2's DROP of + // the fruit table already committed. if matches!(db.get_database_backend(), DbBackend::Postgres) { println!("\nRoll back changes when encounter errors"); - // Set a flag to throw error inside `m20230109_000001_seed_cake_table.rs` - std::env::set_var("ABORT_MIGRATION", "YES"); + // Set a flag to throw error inside `m20220118_000001_create_cake_table.rs` + unsafe { + std::env::set_var("ABORT_MIGRATION", "YES"); + } // Should throw an error println!("\nMigrator::down"); assert_eq!( - Migrator::down(db, None).await, + migrator.down(db, None).await, Err(DbErr::Migration( "Abort migration and rollback changes".into() )) ); println!("\nMigrator::status"); - Migrator::status(db).await?; + migrator.status(db).await?; - // Check migrations have been rolled back + // Only migration 1's down was rolled back (cake table restored). + // Migrations 2-6 were rolled back successfully (fruit table dropped). assert!(manager.has_table("cake").await?); - assert!(manager.has_table("fruit").await?); + assert!(!manager.has_table("fruit").await?); // Unset the flag - std::env::remove_var("ABORT_MIGRATION"); + unsafe { + std::env::remove_var("ABORT_MIGRATION"); + } } println!("\nMigrator::down"); - Migrator::down(db, None).await?; + migrator.down(db, None).await?; assert!(manager.has_table(migration_table_name).await?); if migration_table_name != "seaql_migrations" { @@ -234,25 +270,106 @@ where assert!(!manager.has_table("fruit").await?); println!("\nMigrator::fresh"); - Migrator::fresh(db).await?; + migrator.fresh(db).await?; assert!(manager.has_table("cake").await?); assert!(manager.has_table("fruit").await?); println!("\nMigrator::refresh"); - Migrator::refresh(db).await?; + migrator.refresh(db).await?; assert!(manager.has_table("cake").await?); assert!(manager.has_table("fruit").await?); println!("\nMigrator::reset"); - Migrator::reset(db).await?; + migrator.reset(db).await?; assert!(!manager.has_table("cake").await?); assert!(!manager.has_table("fruit").await?); println!("\nMigrator::status"); - Migrator::status(db).await?; + migrator.status(db).await?; + + Ok(()) +} + +async fn run_transaction_test(url: &str, db_name: &str, schema: &str) -> Result<(), DbErr> { + let db = &create_db(url, db_name, schema).await?; + let backend = db.get_database_backend(); + let manager = SchemaManager::new(db); + + // use_transaction = None: Postgres wraps by default, others don't. + // The assertion happens inside the migration's up()/down() body. + println!("\nTransaction test: use_transaction = None"); + let m = transaction_test::Migrator { + use_transaction: None, + should_fail: false, + }; + m.up(db, None).await?; + assert!(manager.has_table("test_table").await?); + m.down(db, None).await?; + assert!(!manager.has_table("test_table").await?); + m.reset(db).await.ok(); + + // use_transaction = Some(true): forces transaction on every backend. + println!("\nTransaction test: use_transaction = Some(true)"); + let m = transaction_test::Migrator { + use_transaction: Some(true), + should_fail: false, + }; + m.up(db, None).await?; + assert!(manager.has_table("test_table").await?); + m.down(db, None).await?; + assert!(!manager.has_table("test_table").await?); + m.reset(db).await.ok(); + + // use_transaction = Some(false): disables transaction, including on Postgres. + println!("\nTransaction test: use_transaction = Some(false)"); + let m = transaction_test::Migrator { + use_transaction: Some(false), + should_fail: false, + }; + m.up(db, None).await?; + assert!(manager.has_table("test_table").await?); + m.down(db, None).await?; + assert!(!manager.has_table("test_table").await?); + m.reset(db).await.ok(); + + // Failure with transaction: DDL rolled back (except MySQL which auto-commits DDL). + println!("\nTransaction test: failure with transaction"); + let m = transaction_test::Migrator { + use_transaction: Some(true), + should_fail: true, + }; + assert!(m.up(db, None).await.is_err()); + if backend != DbBackend::MySql { + assert!( + !manager.has_table("test_table").await?, + "DDL should be rolled back" + ); + } + m.reset(db).await.ok(); + + // Failure without transaction: DDL persists. + println!("\nTransaction test: failure without transaction"); + let m = transaction_test::Migrator { + use_transaction: Some(false), + should_fail: true, + }; + assert!(m.up(db, None).await.is_err()); + assert!(manager.has_table("test_table").await?, "DDL should persist"); + db.execute_unprepared("DROP TABLE IF EXISTS test_table") + .await?; + m.reset(db).await.ok(); + + // Manual transaction via manager.begin() / commit(). + println!("\nTransaction test: manual begin/commit"); + let m = transaction_test::ManualTxnMigrator; + m.up(db, None).await?; + assert!(manager.has_table("manual_txn_table").await?); + m.down(db, None).await?; + assert!(!manager.has_table("manual_txn_table").await?); + m.reset(db).await.ok(); Ok(()) } diff --git a/sea-orm-migration/tests/postgres.rs b/sea-orm-migration/tests/postgres.rs new file mode 100644 index 0000000000..475fca9922 --- /dev/null +++ b/sea-orm-migration/tests/postgres.rs @@ -0,0 +1,68 @@ +mod common; + +#[cfg(all(test, feature = "sqlx-postgres"))] +mod inner { + use crate::common::migrator::default::*; + use sea_orm::{ConnectOptions, ConnectionTrait, Database, DbBackend, Statement, error::DbErr}; + use sea_orm_migration::prelude::*; + + #[tokio::test] + async fn test_fresh_with_extension() -> Result<(), DbErr> { + let url = + &std::env::var("DATABASE_URL").expect("Environment variable 'DATABASE_URL' not set"); + let db_name = "test_fresh_with_extension"; + + let db_connect = |url: String| async { + let connect_options = ConnectOptions::new(url).to_owned(); + Database::connect(connect_options).await + }; + + let db = db_connect(url.to_owned()).await?; + if !matches!(db.get_database_backend(), DbBackend::Postgres) { + return Ok(()); + } + + db.execute_unprepared(&format!(r#"DROP DATABASE IF EXISTS "{db_name}""#)) + .await?; + db.execute_unprepared(&format!(r#"CREATE DATABASE "{db_name}""#)) + .await?; + + let url = format!("{url}/{db_name}"); + let db = db_connect(url).await?; + + // Create the extension and a custom type + db.execute_unprepared("CREATE EXTENSION IF NOT EXISTS citext") + .await?; + db.execute_unprepared("CREATE TYPE \"UserFruit\" AS ENUM ('Apple', 'Banana')") + .await?; + + // Run the fresh migration + Migrator::fresh(&db).await?; + + // Check that the custom type was dropped and the extension's type was not + let citext_exists: Option = db + .query_one_raw(Statement::from_string( + DbBackend::Postgres, + r#"SELECT 1 as "value" FROM pg_type WHERE typname = 'citext'"#.to_owned(), + )) + .await? + .map(|row| row.try_get("", "value").unwrap()); + + assert_eq!(citext_exists, Some(1), "the citext type should still exist"); + + let user_fruit_exists: Option = db + .query_one_raw(Statement::from_string( + DbBackend::Postgres, + r#"SELECT 1 as "value" FROM pg_type WHERE typname = 'UserFruit'"#.to_owned(), + )) + .await? + .map(|row| row.try_get("", "value").unwrap()); + + assert_eq!( + user_fruit_exists, None, + "the UserFruit type should have been dropped" + ); + + Ok(()) + } +} diff --git a/sea-orm-rocket/Cargo.toml b/sea-orm-rocket/Cargo.toml index 4975f8e106..fc50511786 100644 --- a/sea-orm-rocket/Cargo.toml +++ b/sea-orm-rocket/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["codegen", "lib"] \ No newline at end of file +members = ["codegen", "lib"] diff --git a/sea-orm-rocket/README.md b/sea-orm-rocket/README.md index f8d94b21f9..01dacb7fed 100644 --- a/sea-orm-rocket/README.md +++ b/sea-orm-rocket/README.md @@ -1 +1,11 @@ -# SeaORM Rocket support crate. \ No newline at end of file +# SeaORM Rocket support crate. + +Not all versions of `sea-orm-rocket` are compatible with every version of Rocket +due to breaking changes in the Rocket database management API. Please reference +the below table to determine which version of `sea-orm-rocket` is appropriate +for your use case. + +| Rocket version | `sea-orm-rocket` version | +| :------------- | -----------------------: | +| v0.5.0 (+ RCs) | v0.5.5 | +| v0.5.1 | v0.6.0 | diff --git a/sea-orm-rocket/codegen/Cargo.toml b/sea-orm-rocket/codegen/Cargo.toml index 03e34ab0e6..c5013a393f 100644 --- a/sea-orm-rocket/codegen/Cargo.toml +++ b/sea-orm-rocket/codegen/Cargo.toml @@ -1,22 +1,23 @@ [package] -name = "sea-orm-rocket-codegen" -version = "0.5.0" -authors = ["Sergio Benitez ", "Jeb Rosen "] -description = "Procedural macros for sea_orm_rocket." -repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" -readme = "../README.md" -keywords = ["rocket", "framework", "database", "pools"] -license = "MIT OR Apache-2.0" -edition = "2021" +authors = ["Sergio Benitez ", "Jeb Rosen "] +description = "Procedural macros for sea_orm_rocket." +edition = "2024" +keywords = ["rocket", "framework", "database", "pools"] +license = "MIT OR Apache-2.0" +name = "sea-orm-rocket-codegen" +readme = "../README.md" +repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" +rust-version = "1.85.0" +version = "0.6.0" [lib] proc-macro = true [dependencies] -devise = "0.3" -quote = "1" +devise = "0.4" +quote = "1" [dev-dependencies] -rocket = { version = "0.5.0-rc.1", default-features = false } -trybuild = "1.0" +rocket = { version = "0.5.1", default-features = false } +trybuild = "1" version_check = "0.9" diff --git a/sea-orm-rocket/codegen/src/database.rs b/sea-orm-rocket/codegen/src/database.rs index 360c847427..f1a95a5faa 100644 --- a/sea-orm-rocket/codegen/src/database.rs +++ b/sea-orm-rocket/codegen/src/database.rs @@ -14,7 +14,7 @@ struct DatabaseAttribute { } pub fn derive_database(input: TokenStream) -> TokenStream { - DeriveGenerator::build_for(input, quote!(impl sea_orm_rocket::Database)) + DeriveGenerator::build_for(input, quote!(impl rocket_db_pools::Database)) .support(Support::TupleStruct) .validator(ValidatorBuild::new().struct_validate(|_, s| { if s.fields.len() == 1 { @@ -31,7 +31,7 @@ pub fn derive_database(input: TokenStream) -> TokenStream { let decorated_type = &s.ident; let db_ty = quote_spanned!(decorated_type.span() => - <#decorated_type as sea_orm_rocket::Database> + <#decorated_type as rocket_db_pools::Database> ); quote_spanned! { decorated_type.span() => @@ -64,7 +64,7 @@ pub fn derive_database(input: TokenStream) -> TokenStream { ) -> rocket::request::Outcome { match #db_ty::fetch(req.rocket()) { Some(db) => rocket::outcome::Outcome::Success(db), - None => rocket::outcome::Outcome::Failure(( + None => rocket::outcome::Outcome::Error(( rocket::http::Status::InternalServerError, ())) } } @@ -95,8 +95,8 @@ pub fn derive_database(input: TokenStream) -> TokenStream { const NAME: &'static str = #db_name; - fn init() -> sea_orm_rocket::Initializer { - sea_orm_rocket::Initializer::with_name(#fairing_name) + fn init() -> rocket_db_pools::Initializer { + rocket_db_pools::Initializer::with_name(#fairing_name) } }) })) diff --git a/sea-orm-rocket/lib/Cargo.toml b/sea-orm-rocket/lib/Cargo.toml index 57ad8b6ace..ebcf139884 100644 --- a/sea-orm-rocket/lib/Cargo.toml +++ b/sea-orm-rocket/lib/Cargo.toml @@ -1,31 +1,32 @@ [package] -name = "sea-orm-rocket" -version = "0.5.2" -authors = ["Sergio Benitez ", "Jeb Rosen "] -description = "SeaORM Rocket support crate" -repository = "https://github.com/SeaQL/sea-orm" -readme = "../README.md" -keywords = ["rocket", "framework", "database", "pools"] -license = "MIT OR Apache-2.0" -edition = "2021" +authors = ["Sergio Benitez ", "Jeb Rosen "] +description = "SeaORM Rocket support crate" +edition = "2024" +keywords = ["rocket", "framework", "database", "pools"] +license = "MIT OR Apache-2.0" +name = "sea-orm-rocket" +readme = "../README.md" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "0.6.0" [package.metadata.docs.rs] all-features = true [dependencies.rocket] -version = "0.5.0-rc.1" default-features = false +version = "0.5.1" [dependencies.sea-orm-rocket-codegen] -path = "../codegen" -version = "0.5.0" +path = "../codegen" +version = "0.6.0" [dependencies.rocket_okapi] -version = "0.8.0-rc.2" default-features = false -optional = true +optional = true +version = "0.9" [dev-dependencies.rocket] -version = "0.5.0-rc.1" default-features = false -features = ["json"] \ No newline at end of file +features = ["json"] +version = "0.5.1" diff --git a/sea-orm-rocket/lib/src/config.rs b/sea-orm-rocket/lib/src/config.rs index 2f29f2bbf0..ee83721d62 100644 --- a/sea-orm-rocket/lib/src/config.rs +++ b/sea-orm-rocket/lib/src/config.rs @@ -85,5 +85,6 @@ pub struct Config { pub idle_timeout: Option, /// Enable SQLx statement logging (default true) + #[serde(default)] pub sqlx_logging: bool, } diff --git a/sea-orm-rocket/lib/src/database.rs b/sea-orm-rocket/lib/src/database.rs index 54f6da34d8..23b6cb971d 100644 --- a/sea-orm-rocket/lib/src/database.rs +++ b/sea-orm-rocket/lib/src/database.rs @@ -4,14 +4,14 @@ use std::ops::DerefMut; use rocket::fairing::{self, Fairing, Info, Kind}; use rocket::http::Status; use rocket::request::{FromRequest, Outcome, Request}; -use rocket::{error, info_, Build, Ignite, Phase, Rocket, Sentinel}; +use rocket::{Build, Ignite, Phase, Rocket, Sentinel, error, info_}; use rocket::figment::providers::Serialized; use rocket::yansi::Paint; #[cfg(feature = "rocket_okapi")] use rocket_okapi::{ - gen::OpenApiGenerator, + r#gen::OpenApiGenerator, request::{OpenApiFromRequest, RequestHeaderInput}, }; @@ -131,10 +131,10 @@ pub trait Database: } let dbtype = std::any::type_name::(); - let fairing = Paint::default(format!("{dbtype}::init()")).bold(); + let fairing = Paint::new(format!("{dbtype}::init()")).bold(); error!( "Attempted to fetch unattached database `{}`.", - Paint::default(dbtype).bold() + Paint::new(dbtype).bold() ); info_!( "`{}` fairing must be attached prior to using this database.", @@ -180,7 +180,7 @@ pub struct Initializer(Option<&'static str>, PhantomData D> /// status `InternalServerError`. A [`Sentinel`] guards this condition, and so /// this type of failure is unlikely to occur. A `None` error is returned. /// * If a connection is not available within `connect_timeout` seconds or -/// another error occurs, the gaurd _fails_ with status `ServiceUnavailable` +/// another error occurs, the guard _fails_ with status `ServiceUnavailable` /// and the error is returned in `Some`. pub struct Connection<'a, D: Database>(&'a ::Connection); @@ -261,7 +261,7 @@ impl<'r, D: Database> FromRequest<'r> for Connection<'r, D> { async fn from_request(req: &'r Request<'_>) -> Outcome { match D::fetch(req.rocket()) { Some(pool) => Outcome::Success(Connection(pool.borrow())), - None => Outcome::Failure((Status::InternalServerError, None)), + None => Outcome::Error((Status::InternalServerError, None)), } } } diff --git a/sea-orm-rocket/lib/src/error.rs b/sea-orm-rocket/lib/src/error.rs index ee9b3e5492..b183269f1c 100644 --- a/sea-orm-rocket/lib/src/error.rs +++ b/sea-orm-rocket/lib/src/error.rs @@ -5,10 +5,10 @@ use std::fmt; /// guard. #[derive(Debug)] pub enum Error { - /// An error that occured during database/pool initialization. + /// An error that occurred during database/pool initialization. Init(A), - /// An error that ocurred while retrieving a connection from the pool. + /// An error that occurred while retrieving a connection from the pool. Get(B), /// A [`Figment`](crate::figment::Figment) configuration error. diff --git a/sea-orm-sync/CLAUDE.md b/sea-orm-sync/CLAUDE.md new file mode 100644 index 0000000000..add2ea689f --- /dev/null +++ b/sea-orm-sync/CLAUDE.md @@ -0,0 +1,23 @@ +# sea-orm-sync + +Do **not** edit code in `sea-orm-sync/src/` or `sea-orm-sync/tests/` directly. All source and test files are generated from the main `src/` and `tests/` directories by running: + +```bash +cd /path/to/sea-orm +bash build-tools/make-sync.sh +``` + +The script copies `src/` and `tests/` into `sea-orm-sync/`, then applies sed transforms to strip async/await, remove `#[async_trait]`, replace `futures_util::lock::Mutex` with `std::sync::Mutex`, etc. + +## Workflow + +1. Make all code changes in the **root** `src/` and `tests/` directories. +2. Edit `sea-orm-sync/Cargo.toml` directly if dependency or feature changes are needed. +3. Regenerate sync code: `bash build-tools/make-sync.sh` +4. Build and test: `cd sea-orm-sync && cargo check --features rusqlite` + +## Gotchas + +- **`make-sync.sh` requires full filesystem access.** It uses `sed -i`, `find`, `cp -r`, and `cargo fmt`. Running it inside a sandboxed environment (e.g. Cursor's default sandbox) will silently produce broken output — the sed substitutions won't apply, leaving `async_trait`, `futures_util`, `.await`, etc. in the generated code. Always run with full permissions. +- **The root crate cannot compile with `--features rusqlite`** because it depends on `sea_query_rusqlite`, which is only wired up in `sea-orm-sync/Cargo.toml`. To check rusqlite code, you must regenerate and build from `sea-orm-sync/`. +- **`#[sea_orm_macros::test]`** gates tests on `feature = "rusqlite"` in the sync crate (and on `sqlx-*` features in the async crate). When running tests, pass `--features rusqlite` and set `DATABASE_URL`, e.g.: `DATABASE_URL="sqlite::memory:" cargo test --features rusqlite --test transaction_tests` diff --git a/sea-orm-sync/Cargo.toml b/sea-orm-sync/Cargo.toml new file mode 100644 index 0000000000..29c8c58920 --- /dev/null +++ b/sea-orm-sync/Cargo.toml @@ -0,0 +1,191 @@ +[workspace] +members = [".", "examples/quickstart", "examples/pi_spigot"] + +[package] +authors = ["Chris Tsang "] +categories = ["database"] +description = "🐚 The sync version of SeaORM" +documentation = "https://docs.rs/sea-orm" +edition = "2024" +homepage = "https://www.sea-ql.org/SeaORM" +keywords = ["orm", "mysql", "postgres", "sqlite"] +license = "MIT OR Apache-2.0" +name = "sea-orm-sync" +repository = "https://github.com/SeaQL/sea-orm" +rust-version = "1.85.0" +version = "2.0.0-rc.37" + +[package.metadata.docs.rs] +features = [ + "default", + "mock", + "proxy", + "rbac", + "schema-sync", + "postgres-array", + "postgres-vector", + "with-arrow", +] +rustdoc-args = ["--cfg", "docsrs"] + +[lib] +name = "sea_orm" +path = "src/lib.rs" + +[dependencies] +bigdecimal = { version = "0.4", default-features = false, features = [ + "std", +], optional = true } +chrono = { version = "0.4.30", default-features = false, optional = true } +derive_more = { version = "2", features = ["debug"] } +inventory = { version = "0.3", optional = true } +ipnetwork = { version = "0.20", default-features = false, optional = true } +itertools = "0.14.0" +log = { version = "0.4", default-features = false } +ouroboros = { version = "0.18", default-features = false } +pgvector = { version = "~0.4", default-features = false, optional = true } +rust_decimal = { version = "1", default-features = false, features = [ + "std", +], optional = true } +sea-orm-arrow = { version = "2.0.0-rc", path = "../sea-orm-arrow", default-features = false, optional = true } +sea-orm-macros = { version = "~2.0.0-rc.20", path = "../sea-orm-macros", default-features = false, features = [ + "strum", +] } +sea-query = { version = "=1.0.0-rc.31", default-features = false, features = [ + "thread-safe", + "hashable-value", + "backend-mysql", + "backend-postgres", + "backend-sqlite", + "sea-orm", +] } +sea-query-rusqlite = { version = "0.8.0-rc.14", optional = true } +sea-schema-sync = { version = "0.17.0-rc.15", default-features = false, features = [ + "sync", + "discovery", + "writer", + "probe", +], optional = true } +serde = { version = "1.0", default-features = false } +serde_json = { version = "1.0", default-features = false, optional = true } +strum = { version = "0.27", default-features = false } +thiserror = { version = "2", default-features = false } +time = { version = "0.3.36", default-features = false, optional = true } +tracing = { version = "0.1", default-features = false, features = [ + "attributes", + "log", +] } +url = { version = "2.2", default-features = false } +uuid = { version = "1", default-features = false, optional = true } + +[dev-dependencies] +dotenv = "0.15" +maplit = { version = "1" } +pretty_assertions = { version = "0.7" } +sea-orm-sync = { path = ".", features = [ + "debug-print", + "mock", + "postgres-array", + "tests-cfg", +] } +time = { version = "0.3.36", features = ["macros"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +uuid = { version = "1", features = ["v4"] } + +[features] +debug-print = [] +default = [ + "sync", + "macros", + "with-json", + "with-chrono", + "with-rust_decimal", + "with-uuid", + "with-time", + "sqlite-use-returning-for-3_35", +] +entity-registry = ["inventory", "sea-orm-macros/entity-registry"] +json-array = [ + "postgres-array", +] # this does not actually enable postgres, but only a few traits to support array in sea-query +macros = ["sea-orm-macros/derive"] +mariadb-use-returning = [] +mock = [] +postgres-array = [ + "sea-query/postgres-array", + "sea-orm-macros/postgres-array", + "sea-query-rusqlite?/postgres-array", +] +postgres-vector = [ + "pgvector", + "sea-query/postgres-vector", + "sea-query-rusqlite?/postgres-vector", +] +proxy = ["serde_json", "serde/derive"] +rbac = ["sea-query/audit", "macros"] +rusqlite = ["sea-query-rusqlite/sea-orm", "sea-schema-sync/rusqlite"] +schema-sync = ["sea-schema-sync"] +sea-orm-internal = [] +seaography = ["sea-orm-macros/seaography"] +sqlite-no-row-value-before-3_15 = [] +sqlite-use-returning-for-3_35 = [] +sqlx-dep = [] +sqlx-mysql = [] +sqlx-postgres = [] +sqlx-sqlite = [] +sync = [] +tests-cfg = ["serde/derive"] +tests-features = [ + "default", + "rbac", + "schema-sync", + "with-arrow", + "with-bigdecimal", +] +tracing-spans = [] +with-arrow = ["sea-orm-arrow", "sea-orm-macros/with-arrow"] +with-bigdecimal = [ + "bigdecimal", + "sea-query/with-bigdecimal", + "sea-query-rusqlite?/with-bigdecimal", + "sea-orm-arrow?/with-bigdecimal", +] +with-chrono = [ + "chrono", + "sea-query/with-chrono", + "sea-query-rusqlite?/with-chrono", + "sea-orm-arrow?/with-chrono", +] +with-ipnetwork = [ + "ipnetwork", + "sea-query/with-ipnetwork", + "sea-query-rusqlite?/with-ipnetwork", +] +with-json = [ + "serde_json", + "sea-query/with-json", + "sea-orm-macros/with-json", + "chrono?/serde", + "rust_decimal?/serde", + "bigdecimal?/serde", + "uuid?/serde", + "time?/serde", + "pgvector?/serde", + "sea-query-rusqlite?/with-json", +] +with-rust_decimal = [ + "rust_decimal", + "sea-query/with-rust_decimal", + "sea-query-rusqlite?/with-rust_decimal", + "sea-orm-arrow?/with-rust_decimal", +] +with-time = [ + "time", + "sea-query/with-time", + "sea-query-rusqlite?/with-time", + "sea-orm-arrow?/with-time", +] +with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-rusqlite?/with-uuid"] + +[patch.crates-io] +# sea-query = { path = "../sea-query" } diff --git a/sea-orm-sync/README.md b/sea-orm-sync/README.md new file mode 100644 index 0000000000..4d65973b99 --- /dev/null +++ b/sea-orm-sync/README.md @@ -0,0 +1,551 @@ +
+ + SeaORM + +

+

SeaORM is a powerful ORM for building web services in Rust

+ + [![crate](https://img.shields.io/crates/v/sea-orm.svg)](https://crates.io/crates/sea-orm) + [![build status](https://github.com/SeaQL/sea-orm/actions/workflows/rust.yml/badge.svg)](https://github.com/SeaQL/sea-orm/actions/workflows/rust.yml) + [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) +
Support us with a ⭐ ! + +
+ +# 🐚 SeaORM + +[中文文档](https://github.com/SeaQL/sea-orm/blob/master/README-zh.md) + +### Advanced Relations + +Model complex relationships 1-1, 1-N, M-N, and even self-referential in a high-level, conceptual way. + +### Familiar Concepts + +Inspired by popular ORMs in the Ruby, Python, and Node.js ecosystem, SeaORM offers a developer experience that feels instantly recognizable. + +### Feature Rich + +SeaORM is a batteries-included ORM with filters, pagination, and nested queries to accelerate building REST, GraphQL, and gRPC APIs. + +### Production Ready + +With 250k+ weekly downloads, SeaORM is production-ready, trusted by startups and enterprises worldwide. + +## Getting Started + +[![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) +Join our Discord server to chat with others! + ++ [Documentation](https://www.sea-ql.org/SeaORM) + +Integration examples: + ++ [Actix Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) ++ [Axum Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) ++ [GraphQL Example](https://github.com/SeaQL/sea-orm/tree/master/examples/graphql_example) ++ [jsonrpsee Example](https://github.com/SeaQL/sea-orm/tree/master/examples/jsonrpsee_example) ++ [Loco Example](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_example) / [Loco REST Starter](https://github.com/SeaQL/sea-orm/tree/master/examples/loco_starter) ++ [Poem Example](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) ++ [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) / [Rocket OpenAPI Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_okapi_example) ++ [Salvo Example](https://github.com/SeaQL/sea-orm/tree/master/examples/salvo_example) ++ [Tonic Example](https://github.com/SeaQL/sea-orm/tree/master/examples/tonic_example) ++ [Seaography Example (Bakery)](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) / [Seaography Example (Sakila)](https://github.com/SeaQL/seaography/tree/main/examples/sqlite) + +If you want a simple, clean example that fits in a single file that demonstrates the best of SeaORM, you can try: ++ [Quickstart](https://github.com/SeaQL/sea-orm/blob/master/examples/quickstart/src/main.rs) + +Let's have a quick walk through of the unique features of SeaORM. + +## Expressive Entity format +You don't have to write this by hand! Entity files can be generated from an existing database using `sea-orm-cli`, +following is generated with `--entity-format dense` *(new in 2.0)*. +```rust +mod user { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "user")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub email: String, + #[sea_orm(has_one)] + pub profile: HasOne, + #[sea_orm(has_many)] + pub posts: HasMany, + } +} +mod post { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "post")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub title: String, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub author: HasOne, + #[sea_orm(has_many, via = "post_tag")] // M-N relation with junction + pub tags: HasMany, + } +} +``` + +## Smart Entity Loader +The Entity Loader intelligently uses join for 1-1 and data loader for 1-N relations, +eliminating the N+1 problem even when performing nested queries. +```rust +// join paths: +// user -> profile +// user -> post +// post -> post_tag -> tag +let smart_user = user::Entity::load() + .filter_by_id(42) // shorthand for .filter(user::COLUMN.id.eq(42)) + .with(profile::Entity) // 1-1 uses join + .with((post::Entity, tag::Entity)) // 1-N uses data loader + .one(db) + ? + .unwrap(); + +// 3 queries are executed under the hood: +// 1. SELECT FROM user JOIN profile WHERE id = $ +// 2. SELECT FROM post WHERE user_id IN (..) +// 3. SELECT FROM tag JOIN post_tag WHERE post_id IN (..) + +smart_user + == user::ModelEx { + id: 42, + name: "Bob".into(), + email: "bob@sea-ql.org".into(), + profile: HasOne::Loaded( + profile::ModelEx { + picture: "image.jpg".into(), + } + .into(), + ), + posts: HasMany::Loaded(vec![post::ModelEx { + title: "Nice weather".into(), + tags: HasMany::Loaded(vec![tag::ModelEx { + tag: "sunny".into(), + }]), + }]), + }; +``` + +## ActiveModel: nested persistence made simple +Persist an entire object graph: user, profile (1-1), posts (1-N), and tags (M-N) +in a single operation using a fluent builder API. SeaORM automatically determines +the dependencies and inserts or deletes objects in the correct order. + +```rust +// this creates the nested object as shown above: +let user = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("image.jpg")) + .add_post( + post::ActiveModel::builder() + .set_title("Nice weather") + .add_tag(tag::ActiveModel::builder().set_tag("sunny")), + ) + .save(db) + ?; +``` + +## Schema first or Entity first? Your choice + +SeaORM provides a powerful migration system that lets you create tables, modify schemas, and seed data with ease. + +With SeaORM 2.0, you also get a first-class [Entity First Workflow](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/): +simply define new entities or add columns to existing ones, +and SeaORM will automatically detect the changes and create the new tables, columns, unique keys, and foreign keys. + +```rust +// SeaORM resolves foreign key dependencies and creates the tables in topological order. +// Requires the `entity-registry` and `schema-sync` feature flags. +db.get_schema_registry("my_crate::entity::*").sync(db); +``` + +## Ergonomic Raw SQL + +Let SeaORM handle 95% of your transactional queries. +For the remaining cases that are too complex to express, +SeaORM still offers convenient support for writing raw SQL. +```rust +let user = Item { name: "Bob" }; // nested parameter access +let ids = [2, 3, 4]; // expanded by the `..` operator + +let user: Option = user::Entity::find() + .from_raw_sql(raw_sql!( + Sqlite, + r#"SELECT "id", "name" FROM "user" + WHERE "name" LIKE {user.name} + AND "id" in ({..ids}) + "# + )) + .one(db) + ?; +``` + +## Synchronous Support + +[`sea-orm-sync`](https://crates.io/crates/sea-orm-sync) provides the full SeaORM API without requiring an runtime, making it ideal for lightweight CLI programs with SQLite. + +See the [quickstart example](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-sync/examples/quickstart/src/main.rs) for usage. + +## Basics + +### Select +SeaORM models 1-N and M-N relationships at the Entity level, +letting you traverse many-to-many links through a junction table in a single call. +```rust +// find all models +let cakes: Vec = Cake::find().all(db)?; + +// find and filter +let chocolate: Vec = Cake::find() + .filter(Cake::COLUMN.name.contains("chocolate")) + .all(db) + ?; + +// find one model +let cheese: Option = Cake::find_by_id(1).one(db)?; +let cheese: cake::Model = cheese.unwrap(); + +// find related models (lazy) +let fruit: Option = cheese.find_related(Fruit).one(db)?; + +// find related models (eager): for 1-1 relations +let cake_with_fruit: Vec<(cake::Model, Option)> = + Cake::find().find_also_related(Fruit).all(db)?; + +// find related models (eager): works for both 1-N and M-N relations +let cake_with_fillings: Vec<(cake::Model, Vec)> = Cake::find() + .find_with_related(Filling) // for M-N relations, two joins are performed + .all(db) // rows are automatically consolidated by left entity + ?; +``` +### Nested Select + +Partial models prevent overfetching by letting you querying only the fields +you need; it also makes writing deeply nested relational queries simple. +```rust +use sea_orm::DerivePartialModel; + +#[derive(DerivePartialModel)] +#[sea_orm(entity = "cake::Entity")] +struct CakeWithFruit { + id: i32, + name: String, + #[sea_orm(nested)] + fruit: Option, // this can be a regular or another partial model +} + +let cakes: Vec = Cake::find() + .left_join(fruit::Entity) // no need to specify join condition + .into_partial_model() // only the columns in the partial model will be selected + .all(db) + ?; +``` + +### Insert +SeaORM's ActiveModel lets you work directly with Rust data structures and +persist them through a simple API. +It's easy to insert large batches of rows from different data sources. +```rust +let apple = fruit::ActiveModel { + name: Set("Apple".to_owned()), + ..Default::default() // no need to set primary key +}; + +let pear = fruit::ActiveModel { + name: Set("Pear".to_owned()), + ..Default::default() +}; + +// insert one: Active Record style +let apple = apple.insert(db)?; +apple.id == 1; + +// insert one: repository style +let result = Fruit::insert(apple).exec(db)?; +result.last_insert_id == 1; + +// insert many returning last insert id +let result = Fruit::insert_many([apple, pear]).exec(db)?; +result.last_insert_id == Some(2); +``` + +### Insert (advanced) +You can take advantage of database specific features to perform upsert and idempotent insert. +```rust +// insert many with returning (if supported by database) +let models: Vec = Fruit::insert_many([apple, pear]) + .exec_with_returning(db) + ?; +models[0] + == fruit::Model { + id: 1, // database assigned value + name: "Apple".to_owned(), + cake_id: None, + }; + +// insert with ON CONFLICT on primary key do nothing, with MySQL specific polyfill +let result = Fruit::insert_many([apple, pear]) + .on_conflict_do_nothing() + .exec(db) + ?; + +matches!(result, TryInsertResult::Conflicted); +``` + +### Update +ActiveModel avoids race conditions by updating only the fields you've changed, +never overwriting untouched columns. +You can also craft complex bulk update queries with a fluent query building API. +```rust +use sea_orm::sea_query::{Expr, Value}; + +let pear: Option = Fruit::find_by_id(1).one(db)?; +let mut pear: fruit::ActiveModel = pear.unwrap().into(); + +pear.name = Set("Sweet pear".to_owned()); // update value of a single field + +// update one: only changed columns will be updated +let pear: fruit::Model = pear.update(db)?; + +// update many: UPDATE "fruit" SET "cake_id" = "cake_id" + 2 +// WHERE "fruit"."name" LIKE '%Apple%' +Fruit::update_many() + .col_expr(fruit::COLUMN.cake_id, fruit::COLUMN.cake_id.add(2)) + .filter(fruit::COLUMN.name.contains("Apple")) + .exec(db) + ?; +``` +### Save +You can perform "insert or update" operation with ActiveModel, making it easy to compose transactional operations. +```rust +let banana = fruit::ActiveModel { + id: NotSet, + name: Set("Banana".to_owned()), + ..Default::default() +}; + +// create, because primary key `id` is `NotSet` +let mut banana = banana.save(db)?; + +banana.id == Unchanged(2); +banana.name = Set("Banana Mongo".to_owned()); + +// update, because primary key `id` is present +let banana = banana.save(db)?; +``` +### Delete +The same ActiveModel API consistent with insert and update. +```rust +// delete one: Active Record style +let orange: Option = Fruit::find_by_id(1).one(db)?; +let orange: fruit::Model = orange.unwrap(); +orange.delete(db)?; + +// delete one: repository style +let orange = fruit::ActiveModel { + id: Set(2), + ..Default::default() +}; +fruit::Entity::delete(orange).exec(db)?; + +// delete many: DELETE FROM "fruit" WHERE "fruit"."name" LIKE '%Orange%' +fruit::Entity::delete_many() + .filter(fruit::COLUMN.name.contains("Orange")) + .exec(db) + ?; + +``` +### Raw SQL Query +The `raw_sql!` macro is like the `format!` macro but without the risk of SQL injection. +It supports nested parameter interpolation, array and tuple expansion, and even repeating group, +offering great flexibility in crafting complex queries. + +```rust +#[derive(FromQueryResult)] +struct CakeWithBakery { + name: String, + #[sea_orm(nested)] + bakery: Option, +} + +#[derive(FromQueryResult)] +struct Bakery { + #[sea_orm(alias = "bakery_name")] + name: String, +} + +let cake_ids = [2, 3, 4]; // expanded by the `..` operator + +// can use many APIs with raw SQL, including nested select +let cake: Option = CakeWithBakery::find_by_statement(raw_sql!( + Sqlite, + r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name" + FROM "cake" + LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id" + WHERE "cake"."id" IN ({..cake_ids})"# +)) +.one(db) +?; +``` + +## 🧭 Seaography: instant GraphQL API + +[Seaography](https://github.com/SeaQL/seaography) is a GraphQL framework built for SeaORM. +Seaography allows you to build GraphQL resolvers quickly. +With just a few commands, you can launch a fullly-featured GraphQL server from SeaORM entities, +complete with filter, pagination, relational queries and mutations! + +Look at the [Seaography Example](https://github.com/SeaQL/sea-orm/tree/master/examples/seaography_example) to learn more. + + + +## 🖥️ SeaORM Pro: Professional Admin Panel + +[SeaORM Pro](https://github.com/SeaQL/sea-orm-pro/) is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have! + +SeaORM Pro has been updated to support the latest features in SeaORM 2.0. + +Features: + ++ Full CRUD ++ Built on React + GraphQL ++ Built-in GraphQL resolver ++ Customize the UI with TOML config ++ Role Based Access Control *(new in 2.0)* + +Read the [Getting Started](https://www.sea-ql.org/sea-orm-pro/docs/install-and-config/getting-started/) guide to learn more. + +![](https://raw.githubusercontent.com/SeaQL/sea-orm/refs/heads/master/docs/sea-orm-pro-dark.png#gh-dark-mode-only) +![](https://raw.githubusercontent.com/SeaQL/sea-orm/refs/heads/master/docs/sea-orm-pro-light.png#gh-light-mode-only) + +## SQL Server Support + +[SQL Server for SeaORM](https://www.sea-ql.org/SeaORM-X/) offers the same SeaORM API for MSSQL. We ported all test cases and examples, complemented by MSSQL specific documentation. If you are building enterprise software, you can [request commercial access](https://forms.office.com/r/1MuRPJmYBR). It is currently based on SeaORM 1.0, but we will offer free upgrade to existing users when SeaORM 2.0 is finalized. + +## Releases + +SeaORM 2.0 has reached its release candidate phase. We'd love for you to try it out and help shape the final release by [sharing your feedback](https://github.com/SeaQL/sea-orm/discussions/). + ++ [Change Log](https://github.com/SeaQL/sea-orm/tree/master/CHANGELOG.md) + +SeaORM 2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. + ++ [A Sneak Peek at SeaORM 2.0](https://www.sea-ql.org/blog/2025-09-16-sea-orm-2.0/) ++ [SeaORM 2.0: A closer look](https://www.sea-ql.org/blog/2025-09-24-sea-orm-2.0/) ++ [Role Based Access Control in SeaORM 2.0](https://www.sea-ql.org/blog/2025-09-30-sea-orm-rbac/) ++ [Seaography 2.0: A Powerful and Extensible GraphQL Framework](https://www.sea-ql.org/blog/2025-10-08-seaography/) ++ [SeaORM 2.0: New Entity Format](https://www.sea-ql.org/blog/2025-10-20-sea-orm-2.0/) ++ [SeaORM 2.0: Entity First Workflow](https://www.sea-ql.org/blog/2025-10-30-sea-orm-2.0/) ++ [SeaORM 2.0: Strongly-Typed Column](https://www.sea-ql.org/blog/2025-11-11-sea-orm-2.0/) ++ [What's new in SeaORM Pro 2.0](https://www.sea-ql.org/blog/2025-11-21-whats-new-in-seaormpro-2.0/) ++ [SeaORM 2.0: Nested ActiveModel](https://www.sea-ql.org/blog/2025-11-25-sea-orm-2.0/) ++ [A walk-through of SeaORM 2.0](https://www.sea-ql.org/blog/2025-12-05-sea-orm-2.0/) ++ [How we made SeaORM synchronous](https://www.sea-ql.org/blog/2025-12-12-sea-orm-2.0/) ++ [SeaORM 2.0 Migration Guide](https://www.sea-ql.org/blog/2026-01-12-sea-orm-2.0/) + +If you make extensive use of SeaQuery, we recommend checking out our blog post on SeaQuery 1.0 release: + ++ [The road to SeaQuery 1.0](https://www.sea-ql.org/blog/2025-08-30-sea-query-1.0/) + +## License + +Licensed under either of + +- Apache License, Version 2.0 + ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license + ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +We invite you to participate, contribute and together help build Rust's future. + +A big shout out to our contributors! + +[![Contributors](https://opencollective.com/sea-orm/contributors.svg?width=1000&button=false)](https://github.com/SeaQL/sea-orm/graphs/contributors) + +## Who's using SeaORM? + +Here is a short list of awesome open source software built with SeaORM. Feel free to [submit yours](https://github.com/SeaQL/sea-orm/blob/master/COMMUNITY.md#built-with-seaorm)! + +| Project | GitHub | Tagline | +|---------|--------|---------| +| [Zed](https://github.com/zed-industries/zed) | ![GitHub stars](https://img.shields.io/github/stars/zed-industries/zed.svg?style=social) | A high-performance, multiplayer code editor | +| [OpenObserve](https://github.com/openobserve/openobserve) | ![GitHub stars](https://img.shields.io/github/stars/openobserve/openobserve.svg?style=social) | Open-source observability platform | +| [RisingWave](https://github.com/risingwavelabs/risingwave) | ![GitHub stars](https://img.shields.io/github/stars/risingwavelabs/risingwave.svg?style=social) | Stream processing and management platform | +| [LLDAP](https://github.com/nitnelave/lldap) | ![GitHub stars](https://img.shields.io/github/stars/nitnelave/lldap.svg?style=social) | A light LDAP server for user management | +| [Warpgate](https://github.com/warp-tech/warpgate) | ![GitHub stars](https://img.shields.io/github/stars/warp-tech/warpgate.svg?style=social) | Smart SSH bastion that works with any SSH client | +| [Svix](https://github.com/svix/svix-webhooks) | ![GitHub stars](https://img.shields.io/github/stars/svix/svix-webhooks.svg?style=social) | The enterprise ready webhooks service | +| [Ryot](https://github.com/IgnisDa/ryot) | ![GitHub stars](https://img.shields.io/github/stars/ignisda/ryot.svg?style=social) | The only self hosted tracker you will ever need | +| [Lapdev](https://github.com/lapce/lapdev) | ![GitHub stars](https://img.shields.io/github/stars/lapce/lapdev.svg?style=social) | Self-hosted remote development enviroment | +| [System Initiative](https://github.com/systeminit/si) | ![GitHub stars](https://img.shields.io/github/stars/systeminit/si.svg?style=social) | DevOps Automation Platform | +| [OctoBase](https://github.com/toeverything/OctoBase) | ![GitHub stars](https://img.shields.io/github/stars/toeverything/OctoBase.svg?style=social) | A light-weight, scalable, offline collaborative data backend | + +## Sponsorship + +[SeaQL.org](https://www.sea-ql.org/) is an independent open-source organization run by passionate developers. +If you feel generous, a small donation via [GitHub Sponsor](https://github.com/sponsors/SeaQL) will be greatly appreciated, and goes a long way towards sustaining the organization. + +### Gold Sponsors + + + +
+ +
+ +[QDX](https://qdx.co/) pioneers quantum dynamics-powered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. +We're immensely grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications. + +### Silver Sponsors + +We're grateful to our silver sponsors: Digital Ocean, for sponsoring our servers. And JetBrains, for sponsoring our IDE. + + + + + +
+ + + +
+ +## Mascot + +A friend of Ferris, Terres the hermit crab is the official mascot of SeaORM. His hobby is collecting shells. + +Terres + +## 🦀 Rustacean Sticker Pack +The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish. + +Sticker Pack Contents: + ++ Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography ++ Mascots: Ferris the Crab x 3, Terres the Hermit Crab ++ The Rustacean wordmark + +[Support SeaQL and get a Sticker Pack!](https://www.sea-ql.org/sticker-pack/) All proceeds contributes directly to the ongoing development of SeaQL projects. + +
Rustacean Sticker Pack by SeaQL diff --git a/sea-orm-sync/examples/parquet_example/Cargo.toml b/sea-orm-sync/examples/parquet_example/Cargo.toml new file mode 100644 index 0000000000..1782f31e22 --- /dev/null +++ b/sea-orm-sync/examples/parquet_example/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +# A separate workspace + +[package] +edition = "2024" +name = "sea-orm-parquet-example" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies] +env_logger = { version = "0.11" } +fastrand = "2" +log = { version = "0.4" } +parquet = { version = "57", default-features = false, features = ["arrow"] } + +[dependencies.sea-orm-sync] +features = [ + "rusqlite", + "with-arrow", + "with-chrono", + "with-rust_decimal", +] +path = "../../" diff --git a/sea-orm-sync/examples/parquet_example/src/main.rs b/sea-orm-sync/examples/parquet_example/src/main.rs new file mode 100644 index 0000000000..bfa343a16c --- /dev/null +++ b/sea-orm-sync/examples/parquet_example/src/main.rs @@ -0,0 +1,126 @@ +use sea_orm::{ + ArrowSchema, + entity::*, + prelude::{ChronoUtc, Decimal}, + sea_query::prelude::chrono::Timelike, +}; + +use log::info; + +mod measurement { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "measurement", arrow_schema)] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub recorded_at: ChronoDateTimeUtc, + pub sensor_id: i32, + pub temperature: f64, + #[sea_orm(column_type = "Decimal(Some((10, 4)))")] + pub voltage: Decimal, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +fn main() -> Result<(), Box> { + let env = env_logger::Env::default().filter_or("RUST_LOG", "info,sea_orm=info,sqlx=warn"); + env_logger::Builder::from_env(env).init(); + + // ----------------------------------------------------------------------- + // Step 1: Generate 100 random rows + // ----------------------------------------------------------------------- + let base_ts = ChronoUtc::now(); + let base_ts = base_ts + .with_nanosecond((base_ts.nanosecond() / 1000) * 1000) + .unwrap(); // truncate to microsecond + let mut rng = fastrand::Rng::new(); + + let models: Vec = (1..=100) + .map(|i| { + let offset = std::time::Duration::from_secs(rng.u64(0..86_400)); + let ts = base_ts + offset; + let sensor_id = rng.i32(100..110); + let temperature = -10.0 + rng.f64() * 50.0; // -10 .. 40 °C + let voltage_raw = 30000 + rng.i64(0..5000); // 3.0000 .. 3.5000 + measurement::ActiveModel { + id: Set(i), + recorded_at: Set(ts), + sensor_id: Set(sensor_id), + temperature: Set(temperature), + voltage: Set(Decimal::new(voltage_raw, 4)), + } + }) + .collect(); + + let schema = measurement::Entity::arrow_schema(); + info!("Arrow schema: {schema:?}"); + + // ----------------------------------------------------------------------- + // Step 2: Convert to Arrow RecordBatch and write to Parquet + // ----------------------------------------------------------------------- + let batch = measurement::ActiveModel::to_arrow(&models, &schema)?; + info!( + "RecordBatch: {} rows, {} columns", + batch.num_rows(), + batch.num_columns() + ); + + let parquet_path = "measurements.parquet"; + + { + let file = std::fs::File::create(parquet_path)?; + let mut writer = parquet::arrow::ArrowWriter::try_new(file, schema.into(), None)?; + writer.write(&batch)?; + writer.close()?; + } + info!("Wrote Parquet file: {parquet_path}"); + + // ----------------------------------------------------------------------- + // Step 3: Read the Parquet file back into a RecordBatch + // ----------------------------------------------------------------------- + let file = std::fs::File::open(parquet_path)?; + let reader = + parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder::try_new(file)?.build()?; + + let batches: Vec<_> = reader.collect::>()?; + info!("Read {} batch(es) from Parquet", batches.len()); + + let read_batch = &batches[0]; + assert_eq!(read_batch.num_rows(), 100); + + // Convert back to ActiveModels + let restored = measurement::ActiveModel::from_arrow(read_batch)?; + info!("Restored {} ActiveModels from Parquet", restored.len()); + + for (original, restored) in models.iter().zip(restored.iter()) { + assert_eq!(original, restored, "Roundtrip mismatch"); + } + info!("Parquet roundtrip verified: all rows match."); + + // ----------------------------------------------------------------------- + // Step 4: Dump into SQLite + // ----------------------------------------------------------------------- + + match std::fs::remove_file("measurements.sqlite") { + Ok(_) => (), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => (), + Err(e) => panic!("Failed to remove file: {e}"), + } + + let db = &sea_orm::Database::connect("sqlite://measurements.sqlite")?; + + db.get_schema_builder() + .register(measurement::Entity) + .apply(db)?; + info!("SQLite schema created."); + + measurement::Entity::insert_many(restored).exec(db)?; + info!("Inserted all rows into SQLite."); + + info!("Done!"); + Ok(()) +} diff --git a/sea-orm-sync/examples/pi_spigot/Cargo.toml b/sea-orm-sync/examples/pi_spigot/Cargo.toml new file mode 100644 index 0000000000..f605ecb135 --- /dev/null +++ b/sea-orm-sync/examples/pi_spigot/Cargo.toml @@ -0,0 +1,15 @@ +[package] +edition = "2024" +name = "sea-orm-pi-spigot" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies] +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1" } + +[dependencies.sea-orm-sync] +features = ["rusqlite", "with-json", "schema-sync"] +path = "../../" diff --git a/sea-orm-sync/examples/pi_spigot/README.md b/sea-orm-sync/examples/pi_spigot/README.md new file mode 100644 index 0000000000..84d7ea3073 --- /dev/null +++ b/sea-orm-sync/examples/pi_spigot/README.md @@ -0,0 +1,133 @@ +# Pi Spigot Algorithm Resumable Program Example + +This example shows how easy it is to add SQLite-backed checkpointing to a +long-running computation using SeaORM. The idea applies to any stateful program: +batch jobs, data pipelines, simulations: anything you want to pause and resume. + +We use the Rabinowitz-Wagon pi spigot algorithm as the example workload. It +streams decimal digits of pi one at a time, making it a perfect fit for +demonstrating incremental persistence. + +## The Pattern: State Machine + Serialization + +Any computation that can be modeled as a state machine can be made resumable. +The recipe has four parts: + +``` +new() → initialize fresh state +step() → advance one iteration, mutating &mut self +finalize() → flush any buffered output +to_state() → serialize self into a database row +from_state() → deserialize a database row back into self +``` + +### Step 1: Define your state as a SeaORM entity + +Map every mutable field to a column: + +```rust +pub mod state { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "state")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub digits: u32, // identifies this computation + pub boxes: JsonVec, // algorithm working memory + pub i: u32, // current iteration + pub nines: u32, // buffered 9s + pub predigit: u8, // held digit + pub have_predigit: bool, + pub count: u32, // digits emitted so far + #[sea_orm(column_type = "Text")] + pub result: String, // emitted digits + } +} +``` + +Complex types like `Vec` are stored as JSON columns via `FromJsonQueryResult`. + +### Step 2: Write `to_state` / `from_state` + +These convert between your in-memory struct and the entity model: + +```rust +fn to_state(&self, i: u32) -> state::Model { + state::Model { + digits: self.digits, + boxes: state::JsonVec(self.boxes.clone()), + i, + nines: self.nines, + // ... every field + } +} + +fn from_state(s: state::Model) -> Self { + Self { + digits: s.digits, + boxes: s.boxes.0, + nines: s.nines, + // ... every field + } +} +``` + +### Step 3: Checkpoint with transactions + +Inside your main loop, periodically save state. Use a transaction so the +checkpoint is atomic: either everything is saved or nothing is: + +```rust +if self.count % checkpoint_interval == 0 { + let txn = db.begin()?; + state::Entity::delete_by_id(self.digits).exec(&txn)?; + self.to_state(i + 1).into_active_model().insert(&txn)?; + txn.commit()?; +} +``` + +### Step 4: Resume on startup + +Check for an existing checkpoint. If found, reconstruct from it; otherwise +start fresh: + +```rust +pub fn resume(db: &DatabaseConnection, digits: u32) -> Result { + db.get_schema_builder() + .register(state::Entity) + .sync(db)?; // creates table if it doesn't exist + + match state::Entity::find_by_id(digits).one(db)? { + Some(s) => Ok(Self::from_state(s)), + None => Ok(Self::new(digits)), + } +} +``` + +Note that `get_schema_builder().sync()` creates the table from the entity +definition automatically: no migrations needed. + +## Running the example + +```sh +# Compute 10000 digits of pi (checkpoints every 100 digits to pi.sqlite) +cargo run -- --digits 10000 + +# Press Ctrl-C at any time, then run again: it resumes from the last checkpoint +cargo run -- --digits 10000 + +# Use in-memory SQLite (no persistence, useful for testing) +cargo run -- --digits 1000 --db "sqlite::memory:" +``` + +## Running the tests + +```sh +cargo test +``` + +The tests verify correctness against a known 1000-digit reference, including a +three-phase checkpoint/resume test: checkpoint at iteration 100, resume and +checkpoint again at 500, resume and finish at 1000. diff --git a/sea-orm-sync/examples/pi_spigot/bdigits.html b/sea-orm-sync/examples/pi_spigot/bdigits.html new file mode 100644 index 0000000000..38243c93fe --- /dev/null +++ b/sea-orm-sync/examples/pi_spigot/bdigits.html @@ -0,0 +1,224 @@ + +
+
+3.1415926535 8979323846 2643383279 5028841971 6939937510
+  5820974944 5923078164 0628620899 8628034825 3421170679
+  8214808651 3282306647 0938446095 5058223172 5359408128
+  4811174502 8410270193 8521105559 6446229489 5493038196
+  4428810975 6659334461 2847564823 3786783165 2712019091
+  4564856692 3460348610 4543266482 1339360726 0249141273
+  7245870066 0631558817 4881520920 9628292540 9171536436
+  7892590360 0113305305 4882046652 1384146951 9415116094
+  3305727036 5759591953 0921861173 8193261179 3105118548
+  0744623799 6274956735 1885752724 8912279381 8301194912
+
+  9833673362 4406566430 8602139494 6395224737 1907021798
+  6094370277 0539217176 2931767523 8467481846 7669405132
+  0005681271 4526356082 7785771342 7577896091 7363717872
+  1468440901 2249534301 4654958537 1050792279 6892589235
+  4201995611 2129021960 8640344181 5981362977 4771309960
+  5187072113 4999999837 2978049951 0597317328 1609631859
+  5024459455 3469083026 4252230825 3344685035 2619311881
+  7101000313 7838752886 5875332083 8142061717 7669147303
+  5982534904 2875546873 1159562863 8823537875 9375195778
+  1857780532 1712268066 1300192787 6611195909 2164201989
+
+  3809525720 1065485863 2788659361 5338182796 8230301952
+  0353018529 6899577362 2599413891 2497217752 8347913151
+  5574857242 4541506959 5082953311 6861727855 8890750983
+  8175463746 4939319255 0604009277 0167113900 9848824012
+  8583616035 6370766010 4710181942 9555961989 4676783744
+  9448255379 7747268471 0404753464 6208046684 2590694912
+  9331367702 8989152104 7521620569 6602405803 8150193511
+  2533824300 3558764024 7496473263 9141992726 0426992279
+  6782354781 6360093417 2164121992 4586315030 2861829745
+  5570674983 8505494588 5869269956 9092721079 7509302955
+
+  3211653449 8720275596 0236480665 4991198818 3479775356
+  6369807426 5425278625 5181841757 4672890977 7727938000
+  8164706001 6145249192 1732172147 7235014144 1973568548
+  1613611573 5255213347 5741849468 4385233239 0739414333
+  4547762416 8625189835 6948556209 9219222184 2725502542
+  5688767179 0494601653 4668049886 2723279178 6085784383
+  8279679766 8145410095 3883786360 9506800642 2512520511
+  7392984896 0841284886 2694560424 1965285022 2106611863
+  0674427862 2039194945 0471237137 8696095636 4371917287
+  4677646575 7396241389 0865832645 9958133904 7802759009
+
+  9465764078 9512694683 9835259570 9825822620 5224894077
+  2671947826 8482601476 9909026401 3639443745 5305068203
+  4962524517 4939965143 1429809190 6592509372 2169646151
+  5709858387 4105978859 5977297549 8930161753 9284681382
+  6868386894 2774155991 8559252459 5395943104 9972524680
+  8459872736 4469584865 3836736222 6260991246 0805124388
+  4390451244 1365497627 8079771569 1435997700 1296160894
+  4169486855 5848406353 4220722258 2848864815 8456028506
+  0168427394 5226746767 8895252138 5225499546 6672782398
+  6456596116 3548862305 7745649803 5593634568 1743241125
+
+  1507606947 9451096596 0940252288 7971089314 5669136867
+  2287489405 6010150330 8617928680 9208747609 1782493858
+  9009714909 6759852613 6554978189 3129784821 6829989487
+  2265880485 7564014270 4775551323 7964145152 3746234364
+  5428584447 9526586782 1051141354 7357395231 1342716610
+  2135969536 2314429524 8493718711 0145765403 5902799344
+  0374200731 0578539062 1983874478 0847848968 3321445713
+  8687519435 0643021845 3191048481 0053706146 8067491927
+  8191197939 9520614196 6342875444 0643745123 7181921799
+  9839101591 9561814675 1426912397 4894090718 6494231961
+
+  5679452080 9514655022 5231603881 9301420937 6213785595
+  6638937787 0830390697 9207734672 2182562599 6615014215
+  0306803844 7734549202 6054146659 2520149744 2850732518
+  6660021324 3408819071 0486331734 6496514539 0579626856
+  1005508106 6587969981 6357473638 4052571459 1028970641
+  4011097120 6280439039 7595156771 5770042033 7869936007
+  2305587631 7635942187 3125147120 5329281918 2618612586
+  7321579198 4148488291 6447060957 5270695722 0917567116
+  7229109816 9091528017 3506712748 5832228718 3520935396
+  5725121083 5791513698 8209144421 0067510334 6711031412
+
+  6711136990 8658516398 3150197016 5151168517 1437657618
+  3515565088 4909989859 9823873455 2833163550 7647918535
+  8932261854 8963213293 3089857064 2046752590 7091548141
+  6549859461 6371802709 8199430992 4488957571 2828905923
+  2332609729 9712084433 5732654893 8239119325 9746366730
+  5836041428 1388303203 8249037589 8524374417 0291327656
+  1809377344 4030707469 2112019130 2033038019 7621101100
+  4492932151 6084244485 9637669838 9522868478 3123552658
+  2131449576 8572624334 4189303968 6426243410 7732269780
+  2807318915 4411010446 8232527162 0105265227 2111660396
+
+  6655730925 4711055785 3763466820 6531098965 2691862056
+  4769312570 5863566201 8558100729 3606598764 8611791045
+  3348850346 1136576867 5324944166 8039626579 7877185560
+  8455296541 2665408530 6143444318 5867697514 5661406800
+  7002378776 5913440171 2749470420 5622305389 9456131407
+  1127000407 8547332699 3908145466 4645880797 2708266830
+  6343285878 5698305235 8089330657 5740679545 7163775254
+  2021149557 6158140025 0126228594 1302164715 5097925923
+  0990796547 3761255176 5675135751 7829666454 7791745011
+  2996148903 0463994713 2962107340 4375189573 5961458901
+
+  9389713111 7904297828 5647503203 1986915140 2870808599
+  0480109412 1472213179 4764777262 2414254854 5403321571
+  8530614228 8137585043 0633217518 2979866223 7172159160
+  7716692547 4873898665 4949450114 6540628433 6639379003
+  9769265672 1463853067 3609657120 9180763832 7166416274
+  8888007869 2560290228 4721040317 2118608204 1900042296
+  6171196377 9213375751 1495950156 6049631862 9472654736
+  4252308177 0367515906 7350235072 8354056704 0386743513
+  6222247715 8915049530 9844489333 0963408780 7693259939
+  7805419341 4473774418 4263129860 8099888687 4132604721
+
+  5695162396 5864573021 6315981931 9516735381 2974167729
+  4786724229 2465436680 0980676928 2382806899 6400482435
+  4037014163 1496589794 0924323789 6907069779 4223625082
+  2168895738 3798623001 5937764716 5122893578 6015881617
+  5578297352 3344604281 5126272037 3431465319 7777416031
+  9906655418 7639792933 4419521541 3418994854 4473456738
+  3162499341 9131814809 2777710386 3877343177 2075456545
+  3220777092 1201905166 0962804909 2636019759 8828161332
+  3166636528 6193266863 3606273567 6303544776 2803504507
+  7723554710 5859548702 7908143562 4014517180 6246436267
+
+  9456127531 8134078330 3362542327 8394497538 2437205835
+  3114771199 2606381334 6776879695 9703098339 1307710987
+  0408591337 4641442822 7726346594 7047458784 7787201927
+  7152807317 6790770715 7213444730 6057007334 9243693113
+  8350493163 1284042512 1925651798 0694113528 0131470130
+  4781643788 5185290928 5452011658 3934196562 1349143415
+  9562586586 5570552690 4965209858 0338507224 2648293972
+  8584783163 0577775606 8887644624 8246857926 0395352773
+  4803048029 0058760758 2510474709 1643961362 6760449256
+  2742042083 2085661190 6254543372 1315359584 5068772460
+
+  2901618766 7952406163 4252257719 5429162991 9306455377
+  9914037340 4328752628 8896399587 9475729174 6426357455
+  2540790914 5135711136 9410911939 3251910760 2082520261
+  8798531887 7058429725 9167781314 9699009019 2116971737
+  2784768472 6860849003 3770242429 1651300500 5168323364
+  3503895170 2989392233 4517220138 1280696501 1784408745
+  1960121228 5993716231 3017114448 4640903890 6449544400
+  6198690754 8516026327 5052983491 8740786680 8818338510
+  2283345085 0486082503 9302133219 7155184306 3545500766
+  8282949304 1377655279 3975175461 3953984683 3936383047
+
+  4611996653 8581538420 5685338621 8672523340 2830871123
+  2827892125 0771262946 3229563989 8989358211 6745627010
+  2183564622 0134967151 8819097303 8119800497 3407239610
+  3685406643 1939509790 1906996395 5245300545 0580685501
+  9567302292 1913933918 5680344903 9820595510 0226353536
+  1920419947 4553859381 0234395544 9597783779 0237421617
+  2711172364 3435439478 2218185286 2408514006 6604433258
+  8856986705 4315470696 5747458550 3323233421 0730154594
+  0516553790 6866273337 9958511562 5784322988 2737231989
+  8757141595 7811196358 3300594087 3068121602 8764962867
+
+  4460477464 9159950549 7374256269 0104903778 1986835938
+  1465741268 0492564879 8556145372 3478673303 9046883834
+  3634655379 4986419270 5638729317 4872332083 7601123029
+  9113679386 2708943879 9362016295 1541337142 4892830722
+  0126901475 4668476535 7616477379 4675200490 7571555278
+  1965362132 3926406160 1363581559 0742202020 3187277605
+  2772190055 6148425551 8792530343 5139844253 2234157623
+  3610642506 3904975008 6562710953 5919465897 5141310348
+  2276930624 7435363256 9160781547 8181152843 6679570611
+  0861533150 4452127473 9245449454 2368288606 1340841486
+
+  3776700961 2071512491 4043027253 8607648236 3414334623
+  5189757664 5216413767 9690314950 1910857598 4423919862
+  9164219399 4907236234 6468441173 9403265918 4044378051
+  3338945257 4239950829 6591228508 5558215725 0310712570
+  1266830240 2929525220 1187267675 6220415420 5161841634
+  8475651699 9811614101 0029960783 8690929160 3028840026
+  9104140792 8862150784 2451670908 7000699282 1206604183
+  7180653556 7252532567 5328612910 4248776182 5829765157
+  9598470356 2226293486 0034158722 9805349896 5022629174
+  8788202734 2092222453 3985626476 6914905562 8425039127
+
+  5771028402 7998066365 8254889264 8802545661 0172967026
+  6407655904 2909945681 5065265305 3718294127 0336931378
+  5178609040 7086671149 6558343434 7693385781 7113864558
+  7367812301 4587687126 6034891390 9562009939 3610310291
+  6161528813 8437909904 2317473363 9480457593 1493140529
+  7634757481 1935670911 0137751721 0080315590 2485309066
+  9203767192 2033229094 3346768514 2214477379 3937517034
+  4366199104 0337511173 5471918550 4644902636 5512816228
+  8244625759 1633303910 7225383742 1821408835 0865739177
+  1509682887 4782656995 9957449066 1758344137 5223970968
+
+  3408005355 9849175417 3818839994 4697486762 6551658276
+  5848358845 3142775687 9002909517 0283529716 3445621296
+  4043523117 6006651012 4120065975 5851276178 5838292041
+  9748442360 8007193045 7618932349 2292796501 9875187212
+  7267507981 2554709589 0455635792 1221033346 6974992356
+  3025494780 2490114195 2123828153 0911407907 3860251522
+  7429958180 7247162591 6685451333 1239480494 7079119153
+  2673430282 4418604142 6363954800 0448002670 4962482017
+  9289647669 7583183271 3142517029 6923488962 7668440323
+  2609275249 6035799646 9256504936 8183609003 2380929345
+
+  9588970695 3653494060 3402166544 3755890045 6328822505
+  4525564056 4482465151 8754711962 1844396582 5337543885
+  6909411303 1509526179 3780029741 2076651479 3942590298
+  9695946995 5657612186 5619673378 6236256125 2163208628
+  6922210327 4889218654 3648022967 8070576561 5144632046
+  9279068212 0738837781 4233562823 6089632080 6822246801
+  2248261177 1858963814 0918390367 3672220888 3215137556
+  0037279839 4004152970 0287830766 7094447456 0134556417
+  2543709069 7939612257 1429894671 5435784687 8861444581
+  2314593571 9849225284 7160504922 1242470141 2147805734
+
+  5510500801 9086996033 0276347870 8108175450 1193071412
+  2339086639 3833952942 5786905076 4310063835 1983438934
+  1596131854 3475464955 6978103829 3097164651 4384070070
+  7360411237 3599843452 2516105070 2705623526 6012764848
+  3084076118 3013052793 2054274628 6540360367 4532865105
+  7065874882 2569815793 6789766974 2205750596 8344086973
+  5020141020 6723585020 0724522563 2651341055 9240190274
+  2162484391 4035998953 5394590944 0704691209 1409387001
+  2645600162 3742880210 9276457931 0657922955 2498872758
+  4610126483 6999892256 9596881592 0560010165 5256375679
+
+ diff --git a/sea-orm-sync/examples/pi_spigot/src/lib.rs b/sea-orm-sync/examples/pi_spigot/src/lib.rs new file mode 100644 index 0000000000..1c4e765843 --- /dev/null +++ b/sea-orm-sync/examples/pi_spigot/src/lib.rs @@ -0,0 +1,336 @@ +use sea_orm::{ + ActiveModelTrait, DatabaseConnection, EntityTrait, IntoActiveModel, NotSet, Set, + TransactionTrait, +}; + +pub mod state { + use sea_orm::entity::prelude::*; + use serde::{Deserialize, Serialize}; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "state")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub digits: u32, + pub boxes: JsonVec, + pub i: u32, + pub nines: u32, + pub predigit: u8, + pub have_predigit: bool, + pub count: u32, + #[sea_orm(column_type = "Text")] + pub result: String, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] + pub struct JsonVec(pub Vec); + + impl ActiveModelBehavior for ActiveModel {} +} + +pub mod run_log { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "run_log")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub digits: u32, + #[sea_orm(column_type = "Text")] + pub pi_digits: String, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +/// Tracks the mutable computation state of the spigot algorithm. +pub struct PiSpigot { + digits: u32, + boxes: Vec, + nines: u32, + predigit: u8, + have_predigit: bool, + count: u32, + result: String, + start_i: u32, +} + +impl PiSpigot { + /// Create a new computation for the given number of decimal digits (after "3."). + pub fn new(digits: u32) -> Self { + let len = digits as usize * 10 / 3 + 1; + Self { + digits, + boxes: vec![2u32; len], + nines: 0, + predigit: 0, + have_predigit: false, + count: 0, + result: String::new(), + start_i: 0, + } + } + + /// Load from a checkpoint if one exists, otherwise create fresh. + pub fn resume(db: &DatabaseConnection, digits: u32) -> Result { + db.get_schema_builder() + .register(state::Entity) + .register(run_log::Entity) + .sync(db)?; + + match state::Entity::find_by_id(digits).one(db)? { + Some(s) => { + eprintln!( + "Resuming from checkpoint (iteration {}, {} digits result)", + s.i, s.count + ); + Ok(Self::from_state(s)) + } + None => Ok(Self::new(digits)), + } + } + + /// Compute all digits without database persistence. Returns the decimal + /// digits of pi after "3." (e.g. "14159265..." for 8 digits). + pub fn compute(mut self) -> String { + for _ in 0..=self.digits { + self.step(); + } + self.finalize(); + self.result + } + + /// Compute with database persistence, checkpointing every `checkpoint_interval` iterations. + /// Returns the fractional digits of pi (after "3."). + pub fn compute_with_db( + mut self, + db: &DatabaseConnection, + checkpoint_interval: u32, + ) -> Result { + db.get_schema_builder() + .register(state::Entity) + .register(run_log::Entity) + .sync(db)?; + + let mut run_log = run_log::ActiveModel { + id: NotSet, + digits: Set(self.digits), + pi_digits: Set(String::new()), + } + .save(db)?; + + for i in self.start_i..=self.digits { + self.step(); + + if checkpoint_interval > 0 && self.count > 0 && self.count % checkpoint_interval == 0 { + let txn = db.begin()?; + state::Entity::delete_by_id(self.digits).exec(&txn)?; + self.to_state(i + 1).into_active_model().insert(&txn)?; + run_log.pi_digits = Set(self.result.clone()); + run_log = run_log.save(&txn)?; + txn.commit()?; + let start = self + .result + .len() + .saturating_sub(checkpoint_interval as usize); + eprintln!("[{}] {}", self.count, &self.result[start..]); + } + } + + self.finalize(); + + state::Entity::delete_by_id(self.digits).exec(db)?; + + run_log.pi_digits = Set(self.result.clone()); + run_log.save(db)?; + + Ok(self.result) + } + + /// Resume from a persisted state, recovering previously result digits. + fn from_state(s: state::Model) -> Self { + Self { + digits: s.digits, + boxes: s.boxes.0, + nines: s.nines, + predigit: s.predigit, + have_predigit: s.have_predigit, + count: s.count, + result: s.result, + start_i: s.i, + } + } + + /// Build a state model for checkpoint persistence. + fn to_state(&self, i: u32) -> state::Model { + state::Model { + digits: self.digits, + boxes: state::JsonVec(self.boxes.clone()), + i, + nines: self.nines, + predigit: self.predigit, + have_predigit: self.have_predigit, + count: self.count, + result: self.result.clone(), + } + } + + fn push_digit(&mut self, digit: u8) { + self.result.push((b'0' + digit) as char); + self.count += 1; + } + + /// Run one iteration of the spigot algorithm, producing zero or more digits. + fn step(&mut self) { + let len = self.boxes.len(); + let mut carry: u32 = 0; + + for j in (1..len).rev() { + let j_u = j as u32; + let x = self.boxes[j] * 10 + carry; + self.boxes[j] = x % (2 * j_u + 1); + carry = (x / (2 * j_u + 1)) * j_u; + } + let x = self.boxes[0] * 10 + carry; + let q = (x / 10) as u8; + self.boxes[0] = x % 10; + + if q == 9 { + self.nines += 1; + } else if q == 10 { + if self.have_predigit { + self.push_digit(self.predigit + 1); + } else { + self.push_digit(1); + self.have_predigit = true; + } + for _ in 0..self.nines { + self.push_digit(0); + } + self.predigit = 0; + self.nines = 0; + } else { + if self.have_predigit { + self.push_digit(self.predigit); + } else { + self.have_predigit = true; + } + self.predigit = q; + for _ in 0..self.nines { + self.push_digit(9); + } + self.nines = 0; + } + } + + /// Flush the final predigit and strip the leading "3" so the result + /// contains only fractional digits (e.g. "14159..." not "314159..."). + fn finalize(&mut self) { + self.push_digit(self.predigit); + for _ in 0..self.nines { + self.push_digit(9); + } + self.nines = 0; + // The spigot algorithm produces digits starting with 3; strip it. + if self.result.starts_with('3') { + self.result.remove(0); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const PI_1000: &str = "\ + 14159265358979323846264338327950288419716939937510\ + 58209749445923078164062862089986280348253421170679\ + 82148086513282306647093844609550582231725359408128\ + 48111745028410270193852110555964462294895493038196\ + 44288109756659334461284756482337867831652712019091\ + 45648566923460348610454326648213393607260249141273\ + 72458700660631558817488152092096282925409171536436\ + 78925903600113305305488204665213841469519415116094\ + 33057270365759591953092186117381932611793105118548\ + 07446237996274956735188575272489122793818301194912\ + 98336733624406566430860213949463952247371907021798\ + 60943702770539217176293176752384674818467669405132\ + 00056812714526356082778577134275778960917363717872\ + 14684409012249534301465495853710507922796892589235\ + 42019956112129021960864034418159813629774771309960\ + 51870721134999999837297804995105973173281609631859\ + 50244594553469083026425223082533446850352619311881\ + 71010003137838752886587533208381420617177669147303\ + 59825349042875546873115956286388235378759375195778\ + 18577805321712268066130019278766111959092164201989"; + + #[test] + fn test_compute_10_digits() { + let result = PiSpigot::new(10).compute(); + assert_eq!(&result[..10], &PI_1000[..10]); + } + + #[test] + fn test_compute_100_digits() { + let result = PiSpigot::new(100).compute(); + assert_eq!(&result[..100], &PI_1000[..100]); + } + + #[test] + fn test_compute_1000_digits() { + let result = PiSpigot::new(1000).compute(); + assert_eq!(&result[..1000], PI_1000); + } + + #[test] + fn test_compute_with_db() { + let db = sea_orm::Database::connect("sqlite::memory:").unwrap(); + let spigot = PiSpigot::new(100); + let result = spigot.compute_with_db(&db, 10).unwrap(); + assert_eq!(&result[..100], &PI_1000[..100]); + } + + #[test] + fn test_checkpoint_resume() { + let db = sea_orm::Database::connect("sqlite::memory:").unwrap(); + + db.get_schema_builder() + .register(state::Entity) + .register(run_log::Entity) + .sync(&db) + .unwrap(); + + // Phase 1: step 0..100, checkpoint + let mut spigot = PiSpigot::new(1000); + for _ in 0..100 { + spigot.step(); + } + spigot + .to_state(100) + .into_active_model() + .insert(&db) + .unwrap(); + + // Phase 2: resume, step 100..500, checkpoint again + let mut spigot = PiSpigot::resume(&db, 1000).unwrap(); + assert_eq!(spigot.start_i, 100); + for _ in 100..500 { + spigot.step(); + } + state::Entity::delete_by_id(1000u32).exec(&db).unwrap(); + spigot + .to_state(500) + .into_active_model() + .insert(&db) + .unwrap(); + + // Phase 3: resume from second checkpoint, finish + let resumed = PiSpigot::resume(&db, 1000).unwrap(); + assert_eq!(resumed.start_i, 500); + let result = resumed.compute_with_db(&db, 0).unwrap(); + assert_eq!(result, PI_1000); + } +} diff --git a/sea-orm-sync/examples/pi_spigot/src/main.rs b/sea-orm-sync/examples/pi_spigot/src/main.rs new file mode 100644 index 0000000000..033c38b352 --- /dev/null +++ b/sea-orm-sync/examples/pi_spigot/src/main.rs @@ -0,0 +1,35 @@ +use clap::Parser; +use sea_orm::Database; +use sea_orm_pi_spigot::PiSpigot; + +#[derive(Parser)] +#[command(name = "pi-spigot", about = "Compute digits of pi with checkpointing")] +struct Cli { + /// Number of decimal digits to compute (after "3.") + #[arg(short, long, default_value_t = 100)] + digits: u32, + + /// Checkpoint every N digits (0 to disable) + #[arg(short, long, default_value_t = 100)] + checkpoint: u32, + + /// SQLite database path for persistence + #[arg(long, default_value = "sqlite://pi.sqlite")] + db: String, +} + +fn main() { + let cli = Cli::parse(); + + let db = Database::connect(&cli.db).expect("Failed to connect to database"); + + let spigot = PiSpigot::resume(&db, cli.digits).expect("Failed to initialize"); + + println!("Computing {} decimal digits of pi...", cli.digits); + let result = spigot + .compute_with_db(&db, cli.checkpoint) + .expect("Computation failed"); + + println!("Finished computing {} digits of pi.", cli.digits); + println!("3.{result}"); +} diff --git a/sea-orm-sync/examples/quickstart/Cargo.toml b/sea-orm-sync/examples/quickstart/Cargo.toml new file mode 100644 index 0000000000..0b5341861d --- /dev/null +++ b/sea-orm-sync/examples/quickstart/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2024" +name = "sea-orm-quickstart" +publish = false +rust-version = "1.85.0" +version = "0.1.0" + +[dependencies] +env_logger = { version = "0.11" } +log = { version = "0.4" } + +[dependencies.sea-orm-sync] +features = [ + "rusqlite", + "debug-print", + "entity-registry", + "schema-sync", +] +path = "../../" diff --git a/sea-orm-sync/examples/quickstart/README.md b/sea-orm-sync/examples/quickstart/README.md new file mode 100644 index 0000000000..8884821d1d --- /dev/null +++ b/sea-orm-sync/examples/quickstart/README.md @@ -0,0 +1,3 @@ +# SeaORM Quick Start + +This is a single file app that strives to minimize dependency and code noise. It uses an in-memory SQLite database so there is no setup. Just do `cargo run`! \ No newline at end of file diff --git a/sea-orm-sync/examples/quickstart/src/main.rs b/sea-orm-sync/examples/quickstart/src/main.rs new file mode 100644 index 0000000000..7b412e486f --- /dev/null +++ b/sea-orm-sync/examples/quickstart/src/main.rs @@ -0,0 +1,436 @@ +use log::info; + +mod user { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "user")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub email: String, + #[sea_orm(has_one)] + pub profile: HasOne, + #[sea_orm(has_many)] + pub posts: HasMany, + #[sea_orm(self_ref, via = "user_follower", from = "User", to = "Follower")] + pub followers: HasMany, + #[sea_orm(self_ref, via = "user_follower", reverse)] + pub following: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod profile { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "profile")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub picture: String, + #[sea_orm(unique)] + pub user_id: i32, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod post { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "post")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub title: String, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub author: HasOne, + #[sea_orm(has_many)] + pub comments: HasMany, + #[sea_orm(has_many, via = "post_tag")] + pub tags: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod comment { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "comment")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub comment: String, + pub user_id: i32, + pub post_id: i32, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, + #[sea_orm(belongs_to, from = "post_id", to = "id")] + pub post: HasOne, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod tag { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "tag")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub tag: String, + #[sea_orm(has_many, via = "post_tag")] + pub posts: HasMany, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod post_tag { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] + #[sea_orm(table_name = "post_tag")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub post_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub tag_id: i32, + #[sea_orm(belongs_to, from = "post_id", to = "id")] + pub post: Option, + #[sea_orm(belongs_to, from = "tag_id", to = "id")] + pub tag: Option, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod user_follower { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] + #[sea_orm(table_name = "user_follower")] + pub struct Model { + #[sea_orm(primary_key)] + pub user_id: i32, + #[sea_orm(primary_key)] + pub follower_id: i32, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: Option, + #[sea_orm( + belongs_to, + relation_enum = "Follower", + from = "follower_id", + to = "id" + )] + pub follower: Option, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +fn main() -> Result<(), sea_orm::DbErr> { + ///// Part 0: Setup Environment ///// + + // This disables sqlx's logging and enables sea-orm's logging with parameter injection, + // which is easier to debug. + let env = env_logger::Env::default().filter_or("RUST_LOG", "info,sea_orm=debug,sqlx=warn"); + env_logger::Builder::from_env(env).init(); + + use sea_orm::{entity::*, query::*}; + + // Use a SQLite in memory database so no setup needed. + // SeaORM supports MySQL, Postgres, SQL Server as well. + let db = &sea_orm::Database::connect("sqlite::memory:")?; + + // Populate this fresh database with tables. + // + // All entities defined in this crate are automatically registered + // into the schema registry, regardless of which module they live in. + // + // The registry may also include entities from upstream crates, + // so here we restrict it to entities defined in this crate only. + // + // The order of entity definitions does not matter. + // SeaORM resolves foreign key dependencies automatically + // and creates the tables in the correct order with their keys. + db.get_schema_registry("sea_orm_quickstart::*").sync(db)?; + + info!("Schema created."); + + ///// Part 1: CRUD with nested 1-1 and 1-N relations ///// + + info!("Create user Bob with a profile:"); + let bob = user::ActiveModel::builder() + .set_name("Bob") + .set_email("bob@sea-ql.org") + .set_profile(profile::ActiveModel::builder().set_picture("Tennis")) + .insert(db)?; + + info!("Find Bob by email:"); + assert_eq!( + bob, + // this method is generated by #[sea_orm::model] on unique keys + user::Entity::find_by_email("bob@sea-ql.org") + .one(db)? + .unwrap() + ); + + info!("Query user with profile in a single query:"); + let mut bob = user::Entity::load() + .filter_by_id(bob.id) + .with(profile::Entity) + .one(db)? + .expect("Not found"); + assert_eq!(bob.name, "Bob"); + assert_eq!(bob.profile.as_ref().unwrap().picture, "Tennis"); + + // Here we take ownership of the nested model, modify in place and save it + info!("Update Bob's profile:"); + bob.profile + .take() + .unwrap() + .into_active_model() + .set_picture("Landscape") + .save(db)?; + + info!("Confirmed that it's been updated:"); + assert_eq!( + profile::Entity::find_by_user_id(bob.id).all(db)?[0].picture, + "Landscape" + ); + + // we don't have to set the `user_id` of the posts, they're automatically set to Bob + info!("Bob wrote some posts:"); + let mut bob = bob.into_active_model(); + bob.posts + .push( + post::ActiveModel::builder() + .set_title("Lorem ipsum dolor sit amet, consectetur adipiscing elit"), + ) + .push( + post::ActiveModel::builder() + .set_title("Ut enim ad minim veniam, quis nostrud exercitation"), + ); + bob.save(db)?; + + info!("Find Bob's profile and his posts:"); + let bob = user::Entity::load() + .filter(user::COLUMN.name.eq("Bob")) + .with(profile::Entity) + .with(post::Entity) + .one(db)? + .unwrap(); + + assert_eq!(bob.name, "Bob"); + assert_eq!(bob.profile.as_ref().unwrap().picture, "Landscape"); + assert!(bob.posts[0].title.starts_with("Lorem ipsum")); + assert!(bob.posts[1].title.starts_with("Ut enim ad")); + + // It's actually fine to create user + profile the other way round. + // SeaORM figures out the dependency and creates the user first. + info!("Create a new user Alice:"); + let alice = profile::ActiveModel::builder() + .set_user( + user::ActiveModel::builder() + .set_name("Alice") + .set_email("alice@rust-lang.org"), + ) + .set_picture("Park") + .insert(db)? + .user + .unwrap(); + + // Not only can we insert new posts via the bob active model, + // we can also add new comments to the posts. + // SeaORM walks the document tree and figures out what's changed, + // and perform the operation in one transaction. + let mut bob = bob.into_active_model(); + info!("Alice commented on Bob's post:"); + bob.posts[0].comments.push( + comment::ActiveModel::builder() + .set_comment("nice post!") + .set_user_id(alice.id), + ); + bob.posts[1].comments.push( + comment::ActiveModel::builder() + .set_comment("interesting!") + .set_user_id(alice.id), + ); + let bob = bob.save(db)?; + + info!("Find all posts with author along with comments and who commented:"); + let posts = post::Entity::load() + .with(user::Entity) + .with((comment::Entity, user::Entity)) + .all(db)?; + + assert!(posts[0].title.starts_with("Lorem ipsum")); + assert_eq!(posts[0].author.as_ref().unwrap().name, "Bob"); + assert_eq!(posts[0].comments.len(), 1); + assert_eq!(posts[0].comments[0].comment, "nice post!"); + assert_eq!(posts[0].comments[0].user.as_ref().unwrap().name, "Alice"); + + assert!(posts[1].title.starts_with("Ut enim ad")); + assert_eq!(posts[1].author.as_ref().unwrap().name, "Bob"); + assert_eq!(posts[1].comments.len(), 1); + assert_eq!(posts[1].comments[0].comment, "interesting!"); + + // Again, we can apply multiple changes in one operation, + // the queries are executed inside a transaction. + info!("Update post title and comment on first post:"); + let mut post = posts[0].clone().into_active_model(); + post.title = Set("Lorem ipsum dolor sit amet".into()); // shorten it + post.comments[0].comment = Set("nice post! I learnt a lot".into()); + post.save(db)?; + + info!("Confirm the post and comment is updated"); + let post = post::Entity::load() + .filter_by_id(posts[0].id) + .with(comment::Entity) + .one(db)? + .unwrap(); + + assert_eq!(post.title, "Lorem ipsum dolor sit amet"); + assert_eq!(post.comments[0].comment, "nice post! I learnt a lot"); + + // Comments belongs to post. They will be deleted first, otherwise the foreign key + // would prevent the operation. + info!("Delete the post along with all comments"); + post.delete(db)?; + + assert!(post::Entity::find_by_id(posts[0].id).one(db)?.is_none()); + + ///// Part 2: managing M-N relations ///// + + // A unique feature of SeaORM is modelling many-to-many relations in a high level way + + info!("Insert one tag for later use"); + let sunny = tag::ActiveModel::builder().set_tag("sunny").save(db)?; + + info!("Insert a new post with 2 tags"); + let mut post = post::ActiveModel::builder() + .set_title("A perfect day out") + .set_user_id(alice.id) + .add_tag(sunny.clone()) // an existing tag + .add_tag(tag::ActiveModel::builder().set_tag("foodie")) // a new tag + .save(db) // new tag will be created and associcated to the new post + ?; + + let post_id = post.id.clone().unwrap(); + + { + info!("get back the post and tags"); + let post = post::Entity::load() + .filter_by_id(post_id) + .with(tag::Entity) + .one(db)? + .unwrap(); + assert_eq!(post.title, "A perfect day out"); + assert_eq!(post.tags.len(), 2); + assert_eq!(post.tags[0].tag, "sunny"); + assert_eq!(post.tags[1].tag, "foodie"); + } + + info!("Add new tag to post"); + post.tags + .push(tag::ActiveModel::builder().set_tag("downtown")); + let mut post = post.save(db)?; + + { + info!("get back the post and tags"); + let post = post::Entity::load() + .filter_by_id(post_id) + .with(tag::Entity) + .one(db)? + .unwrap(); + assert_eq!(post.tags.len(), 3); + assert_eq!(post.tags[0].tag, "sunny"); + assert_eq!(post.tags[1].tag, "foodie"); + assert_eq!(post.tags[2].tag, "downtown"); + } + + info!("Update post title and remove a tag"); + let mut tags = post.tags.take(); + tags.as_mut_vec().remove(0); // it actually rained + post.title = Set("Almost a perfect day out".into()); + post.tags.replace_all(tags); + // converting the field from append to replace would delete associations not in this list + let post = post.save(db)?; + + { + info!("get back the post and tags"); + let post = post::Entity::load() + .filter_by_id(post_id) + .with(tag::Entity) + .one(db)? + .unwrap(); + assert_eq!(post.tags.len(), 2); + assert_eq!(post.title, "Almost a perfect day out"); + assert_eq!(post.tags[0].tag, "foodie"); + assert_eq!(post.tags[1].tag, "downtown"); + } + + // only the association between post and tag is removed, + // but the tag itself is not deleted + info!("check that the tag sunny still exists"); + assert!(tag::Entity::find_by_tag("sunny").one(db)?.is_some()); + + info!("cascade delete post, remove tag associations"); + post.delete(db)?; + + ///// Part 3: self-referencing relations ///// + + info!("save a new user with a new profile"); + let sam = user::ActiveModel::builder() + .set_name("Sam") + .set_email("sam@rustacean.net") + .set_profile(profile::ActiveModel::builder().set_picture("Crab.jpg")) + .save(db)?; + + // we can do it from left to right: user <- follower + info!("Add follower to Alice"); + let alice = alice.into_active_model().add_follower(bob).save(db)?; + + // we can also do it in reverse: user -> following + info!("Sam starts following Alice"); + sam.add_following(alice).save(db)?; + + info!("Query Alice's profile and followers"); + let alice = user::Entity::load() + .filter_by_email("alice@rust-lang.org") + .with(profile::Entity) + .with(user_follower::Entity) + .with(user_follower::Entity::REVERSE) + .one(db)? + .unwrap(); + + assert_eq!(alice.name, "Alice"); + assert_eq!(alice.profile.as_ref().unwrap().picture, "Park"); + assert_eq!(alice.followers.len(), 2); + assert_eq!(alice.followers[0].name, "Bob"); + assert_eq!(alice.followers[1].name, "Sam"); + + Ok(()) +} diff --git a/sea-orm-sync/src/database/connection.rs b/sea-orm-sync/src/database/connection.rs new file mode 100644 index 0000000000..696e48e80d --- /dev/null +++ b/sea-orm-sync/src/database/connection.rs @@ -0,0 +1,204 @@ +use crate::{ + DbBackend, DbErr, ExecResult, QueryResult, Statement, StatementBuilder, TransactionError, +}; + +/// The generic API for a database connection that can perform query or execute statements. +/// It abstracts database connection and transaction +pub trait ConnectionTrait { + /// Get the database backend for the connection. This depends on feature flags enabled. + fn get_database_backend(&self) -> DbBackend; + + /// Execute a [Statement] + fn execute_raw(&self, stmt: Statement) -> Result; + + /// Execute a [QueryStatement] + fn execute(&self, stmt: &S) -> Result { + let db_backend = self.get_database_backend(); + let stmt = db_backend.build(stmt); + self.execute_raw(stmt) + } + + /// Execute a unprepared [Statement] + fn execute_unprepared(&self, sql: &str) -> Result; + + /// Execute a [Statement] and return a single row of `QueryResult` + fn query_one_raw(&self, stmt: Statement) -> Result, DbErr>; + + /// Execute a [QueryStatement] and return a single row of `QueryResult` + fn query_one(&self, stmt: &S) -> Result, DbErr> { + let db_backend = self.get_database_backend(); + let stmt = db_backend.build(stmt); + self.query_one_raw(stmt) + } + + /// Execute a [Statement] and return a vector of `QueryResult` + fn query_all_raw(&self, stmt: Statement) -> Result, DbErr>; + + /// Execute a [QueryStatement] and return a vector of `QueryResult` + fn query_all(&self, stmt: &S) -> Result, DbErr> { + let db_backend = self.get_database_backend(); + let stmt = db_backend.build(stmt); + self.query_all_raw(stmt) + } + + /// Check if the connection supports `RETURNING` syntax on insert and update + fn support_returning(&self) -> bool { + let db_backend = self.get_database_backend(); + db_backend.support_returning() + } + + /// Check if the connection is a test connection for the Mock database + fn is_mock_connection(&self) -> bool { + false + } +} + +/// Stream query results +pub trait StreamTrait { + /// Create a stream for the [QueryResult] + type Stream<'a>: Iterator> + where + Self: 'a; + + /// Get the database backend for the connection. This depends on feature flags enabled. + fn get_database_backend(&self) -> DbBackend; + + /// Execute a [Statement] and return a stream of results + fn stream_raw<'a>(&'a self, stmt: Statement) -> Result, DbErr>; + + /// Execute a [QueryStatement] and return a stream of results + fn stream<'a, S: StatementBuilder>(&'a self, stmt: &S) -> Result, DbErr> { + let db_backend = self.get_database_backend(); + let stmt = db_backend.build(stmt); + self.stream_raw(stmt) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Isolation level +pub enum IsolationLevel { + /// Consistent reads within the same transaction read the snapshot established by the first read. + RepeatableRead, + /// Each consistent read, even within the same transaction, sets and reads its own fresh snapshot. + ReadCommitted, + /// SELECT statements are performed in a nonlocking fashion, but a possible earlier version of a row might be used. + ReadUncommitted, + /// All statements of the current transaction can only see rows committed before the first query or data-modification statement was executed in this transaction. + Serializable, +} + +impl std::fmt::Display for IsolationLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IsolationLevel::RepeatableRead => write!(f, "REPEATABLE READ"), + IsolationLevel::ReadCommitted => write!(f, "READ COMMITTED"), + IsolationLevel::ReadUncommitted => write!(f, "READ UNCOMMITTED"), + IsolationLevel::Serializable => write!(f, "SERIALIZABLE"), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Access mode +pub enum AccessMode { + /// Data can't be modified in this transaction + ReadOnly, + /// Data can be modified in this transaction (default) + ReadWrite, +} + +impl std::fmt::Display for AccessMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AccessMode::ReadOnly => write!(f, "READ ONLY"), + AccessMode::ReadWrite => write!(f, "READ WRITE"), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Which kind of transaction to start. Only supported by SQLite. +/// +pub enum SqliteTransactionMode { + /// The default. Transaction starts when the next statement is executed, and + /// will be a read or write transaction depending on that statement. + Deferred, + /// Start a write transaction as soon as the BEGIN statement is received. + Immediate, + /// Start a write transaction as soon as the BEGIN statement is received. + /// When in non-WAL mode, also block all other transactions from reading the + /// database. + Exclusive, +} + +impl SqliteTransactionMode { + /// The keyword used to start a transaction in this mode (the word coming after "BEGIN"). + pub fn sqlite_keyword(&self) -> &'static str { + match self { + SqliteTransactionMode::Deferred => "DEFERRED", + SqliteTransactionMode::Immediate => "IMMEDIATE", + SqliteTransactionMode::Exclusive => "EXCLUSIVE", + } + } +} + +/// Configuration for starting a transaction +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +pub struct TransactionOptions { + /// Isolation level for the new transaction + pub isolation_level: Option, + /// Access mode for the new transaction + pub access_mode: Option, + /// Transaction mode (deferred, immediate, exclusive) for the new transaction. Supported only by SQLite. + pub sqlite_transaction_mode: Option, +} + +/// Spawn database transaction +pub trait TransactionTrait { + /// The concrete type for the transaction + type Transaction: ConnectionTrait + TransactionTrait + TransactionSession; + + /// Execute SQL `BEGIN` transaction. + /// Returns a Transaction that can be committed or rolled back + fn begin(&self) -> Result; + + /// Execute SQL `BEGIN` transaction with isolation level and/or access mode. + /// Returns a Transaction that can be committed or rolled back + fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result; + + /// Execute SQL `BEGIN` transaction with isolation level and/or access mode. + /// Returns a Transaction that can be committed or rolled back + fn begin_with_options(&self, options: TransactionOptions) -> Result; + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + fn transaction(&self, callback: F) -> Result> + where + F: for<'c> FnOnce(&'c Self::Transaction) -> Result, + E: std::fmt::Display + std::fmt::Debug; + + /// Execute the function inside a transaction with isolation level and/or access mode. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + fn transaction_with_config( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'c> FnOnce(&'c Self::Transaction) -> Result, + E: std::fmt::Display + std::fmt::Debug; +} + +/// Represents an open transaction +pub trait TransactionSession { + /// Commit a transaction + fn commit(self) -> Result<(), DbErr>; + + /// Rolls back a transaction explicitly + fn rollback(self) -> Result<(), DbErr>; +} diff --git a/sea-orm-sync/src/database/db_connection.rs b/sea-orm-sync/src/database/db_connection.rs new file mode 100644 index 0000000000..95c0bd9870 --- /dev/null +++ b/sea-orm-sync/src/database/db_connection.rs @@ -0,0 +1,808 @@ +use crate::{ + AccessMode, ConnectionTrait, DatabaseTransaction, ExecResult, IsolationLevel, QueryResult, + Schema, SchemaBuilder, Statement, StatementBuilder, StreamTrait, TransactionError, + TransactionOptions, TransactionTrait, error::*, +}; +use std::fmt::Debug; +use tracing::instrument; +use url::Url; + +#[cfg(feature = "sqlx-dep")] +use sqlx::pool::PoolConnection; + +#[cfg(feature = "rusqlite")] +use crate::driver::rusqlite::{RusqliteInnerConnection, RusqliteSharedConnection}; + +#[cfg(any(feature = "mock", feature = "proxy"))] +use std::sync::Arc; + +/// Handle a database connection depending on the backend enabled by the feature +/// flags. This creates a connection pool internally (for SQLx connections), +/// and so is cheap to clone. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct DatabaseConnection { + /// `DatabaseConnection` used to be a enum. Now it's moved into inner, + /// because we have to attach other contexts. + pub inner: DatabaseConnectionType, + #[cfg(feature = "rbac")] + pub(crate) rbac: crate::RbacEngineMount, +} + +/// The underlying database connection type. +#[derive(Clone)] +pub enum DatabaseConnectionType { + /// MySql database connection pool + #[cfg(feature = "sqlx-mysql")] + SqlxMySqlPoolConnection(crate::SqlxMySqlPoolConnection), + + /// PostgreSQL database connection pool + #[cfg(feature = "sqlx-postgres")] + SqlxPostgresPoolConnection(crate::SqlxPostgresPoolConnection), + + /// SQLite database connection pool + #[cfg(feature = "sqlx-sqlite")] + SqlxSqlitePoolConnection(crate::SqlxSqlitePoolConnection), + + /// SQLite database connection sharable across threads + #[cfg(feature = "rusqlite")] + RusqliteSharedConnection(RusqliteSharedConnection), + + /// Mock database connection useful for testing + #[cfg(feature = "mock")] + MockDatabaseConnection(Arc), + + /// Proxy database connection + #[cfg(feature = "proxy")] + ProxyDatabaseConnection(Arc), + + /// The connection has never been established + Disconnected, +} + +/// The same as a [DatabaseConnection] +pub type DbConn = DatabaseConnection; + +impl Default for DatabaseConnection { + fn default() -> Self { + DatabaseConnectionType::Disconnected.into() + } +} + +impl From for DatabaseConnection { + fn from(inner: DatabaseConnectionType) -> Self { + Self { + inner, + #[cfg(feature = "rbac")] + rbac: Default::default(), + } + } +} + +/// The type of database backend for real world databases. +/// This is enabled by feature flags as specified in the crate documentation +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum DatabaseBackend { + /// A MySQL backend + MySql, + /// A PostgreSQL backend + Postgres, + /// A SQLite backend + Sqlite, +} + +/// A shorthand for [DatabaseBackend]. +pub type DbBackend = DatabaseBackend; + +#[derive(Debug)] +pub(crate) enum InnerConnection { + #[cfg(feature = "sqlx-mysql")] + MySql(PoolConnection), + #[cfg(feature = "sqlx-postgres")] + Postgres(PoolConnection), + #[cfg(feature = "sqlx-sqlite")] + Sqlite(PoolConnection), + #[cfg(feature = "rusqlite")] + Rusqlite(RusqliteInnerConnection), + #[cfg(feature = "mock")] + Mock(Arc), + #[cfg(feature = "proxy")] + Proxy(Arc), +} + +impl Debug for DatabaseConnectionType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + #[cfg(feature = "sqlx-mysql")] + Self::SqlxMySqlPoolConnection(_) => "SqlxMySqlPoolConnection", + #[cfg(feature = "sqlx-postgres")] + Self::SqlxPostgresPoolConnection(_) => "SqlxPostgresPoolConnection", + #[cfg(feature = "sqlx-sqlite")] + Self::SqlxSqlitePoolConnection(_) => "SqlxSqlitePoolConnection", + #[cfg(feature = "rusqlite")] + Self::RusqliteSharedConnection(_) => "RusqliteSharedConnection", + #[cfg(feature = "mock")] + Self::MockDatabaseConnection(_) => "MockDatabaseConnection", + #[cfg(feature = "proxy")] + Self::ProxyDatabaseConnection(_) => "ProxyDatabaseConnection", + Self::Disconnected => "Disconnected", + } + ) + } +} + +impl ConnectionTrait for DatabaseConnection { + fn get_database_backend(&self) -> DbBackend { + self.get_database_backend() + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn execute_raw(&self, stmt: Statement) -> Result { + super::tracing_spans::with_db_span!( + "sea_orm.execute", + self.get_database_backend(), + stmt.sql.as_str(), + record_stmt = true, + { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.execute(stmt), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => conn.execute(stmt), + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.execute(stmt), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.execute(stmt), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => conn.execute(stmt), + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => conn.execute(stmt), + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + ) + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn execute_unprepared(&self, sql: &str) -> Result { + super::tracing_spans::with_db_span!( + "sea_orm.execute_unprepared", + self.get_database_backend(), + sql, + record_stmt = false, + { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => { + conn.execute_unprepared(sql) + } + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.execute_unprepared(sql) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => { + conn.execute_unprepared(sql) + } + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => { + conn.execute_unprepared(sql) + } + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + let db_backend = conn.get_database_backend(); + let stmt = Statement::from_string(db_backend, sql); + conn.execute(stmt) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + let db_backend = conn.get_database_backend(); + let stmt = Statement::from_string(db_backend, sql); + conn.execute(stmt) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + ) + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { + super::tracing_spans::with_db_span!( + "sea_orm.query_one", + self.get_database_backend(), + stmt.sql.as_str(), + record_stmt = true, + { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.query_one(stmt), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.query_one(stmt) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.query_one(stmt), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.query_one(stmt), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => conn.query_one(stmt), + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => conn.query_one(stmt), + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + ) + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { + super::tracing_spans::with_db_span!( + "sea_orm.query_all", + self.get_database_backend(), + stmt.sql.as_str(), + record_stmt = true, + { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.query_all(stmt), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.query_all(stmt) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.query_all(stmt), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.query_all(stmt), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => conn.query_all(stmt), + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => conn.query_all(stmt), + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + ) + } + + #[cfg(feature = "mock")] + fn is_mock_connection(&self) -> bool { + matches!( + self, + DatabaseConnection { + inner: DatabaseConnectionType::MockDatabaseConnection(_), + .. + } + ) + } +} + +impl StreamTrait for DatabaseConnection { + type Stream<'a> = crate::QueryStream; + + fn get_database_backend(&self) -> DbBackend { + self.get_database_backend() + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn stream_raw<'a>(&'a self, stmt: Statement) -> Result, DbErr> { + ({ + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.stream(stmt), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => conn.stream(stmt), + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.stream(stmt), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.stream(stmt), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + Ok(crate::QueryStream::from((Arc::clone(conn), stmt, None))) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + Ok(crate::QueryStream::from((Arc::clone(conn), stmt, None))) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + }) + } +} + +impl TransactionTrait for DatabaseConnection { + type Transaction = DatabaseTransaction; + + #[instrument(level = "trace")] + fn begin(&self) -> Result { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.begin(None, None), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => conn.begin(None, None), + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.begin(None, None, None), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.begin(None, None, None), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + DatabaseTransaction::new_mock(Arc::clone(conn), None) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + DatabaseTransaction::new_proxy(conn.clone(), None) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + + #[instrument(level = "trace")] + fn begin_with_config( + &self, + _isolation_level: Option, + _access_mode: Option, + ) -> Result { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => { + conn.begin(_isolation_level, _access_mode) + } + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.begin(_isolation_level, _access_mode) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => { + conn.begin(_isolation_level, _access_mode, None) + } + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => { + conn.begin(_isolation_level, _access_mode, None) + } + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + DatabaseTransaction::new_mock(Arc::clone(conn), None) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + DatabaseTransaction::new_proxy(conn.clone(), None) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + + #[instrument(level = "trace")] + fn begin_with_options( + &self, + TransactionOptions { + isolation_level: _isolation_level, + access_mode: _access_mode, + sqlite_transaction_mode: _sqlite_transaction_mode, + }: TransactionOptions, + ) -> Result { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => { + conn.begin(_isolation_level, _access_mode) + } + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.begin(_isolation_level, _access_mode) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => { + conn.begin(_isolation_level, _access_mode, _sqlite_transaction_mode) + } + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => { + conn.begin(_isolation_level, _access_mode, _sqlite_transaction_mode) + } + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + DatabaseTransaction::new_mock(Arc::clone(conn), None) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + DatabaseTransaction::new_proxy(conn.clone(), None) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + #[instrument(level = "trace", skip(_callback))] + fn transaction(&self, _callback: F) -> Result> + where + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => { + conn.transaction(_callback, None, None) + } + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.transaction(_callback, None, None) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => { + conn.transaction(_callback, None, None) + } + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => { + conn.transaction(_callback, None, None) + } + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + let transaction = DatabaseTransaction::new_mock(Arc::clone(conn), None) + .map_err(TransactionError::Connection)?; + transaction.run(_callback) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + let transaction = DatabaseTransaction::new_proxy(conn.clone(), None) + .map_err(TransactionError::Connection)?; + transaction.run(_callback) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected").into()), + } + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + #[instrument(level = "trace", skip(_callback))] + fn transaction_with_config( + &self, + _callback: F, + _isolation_level: Option, + _access_mode: Option, + ) -> Result> + where + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => { + conn.transaction(_callback, _isolation_level, _access_mode) + } + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.transaction(_callback, _isolation_level, _access_mode) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => { + conn.transaction(_callback, _isolation_level, _access_mode) + } + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => { + conn.transaction(_callback, _isolation_level, _access_mode) + } + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => { + let transaction = DatabaseTransaction::new_mock(Arc::clone(conn), None) + .map_err(TransactionError::Connection)?; + transaction.run(_callback) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => { + let transaction = DatabaseTransaction::new_proxy(conn.clone(), None) + .map_err(TransactionError::Connection)?; + transaction.run(_callback) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected").into()), + } + } +} + +#[cfg(feature = "mock")] +impl DatabaseConnection { + /// Generate a database connection for testing the Mock database + /// + /// # Panics + /// + /// Panics if [DbConn] is not a mock connection. + pub fn as_mock_connection(&self) -> &crate::MockDatabaseConnection { + match &self.inner { + DatabaseConnectionType::MockDatabaseConnection(mock_conn) => mock_conn, + _ => panic!("Not mock connection"), + } + } + + /// Get the transaction log as a collection Vec<[crate::Transaction]> + /// + /// # Panics + /// + /// Panics if the mocker mutex is being held by another thread. + pub fn into_transaction_log(self) -> Vec { + let mut mocker = self + .as_mock_connection() + .get_mocker_mutex() + .lock() + .expect("Fail to acquire mocker"); + mocker.drain_transaction_log() + } +} + +#[cfg(feature = "proxy")] +impl DatabaseConnection { + /// Generate a database connection for testing the Proxy database + /// + /// # Panics + /// + /// Panics if [DbConn] is not a proxy connection. + pub fn as_proxy_connection(&self) -> &crate::ProxyDatabaseConnection { + match &self.inner { + DatabaseConnectionType::ProxyDatabaseConnection(proxy_conn) => proxy_conn, + _ => panic!("Not proxy connection"), + } + } +} + +#[cfg(feature = "rbac")] +impl DatabaseConnection { + /// Load RBAC data from the same database as this connection and setup RBAC engine. + /// If the RBAC engine already exists, it will be replaced. + pub fn load_rbac(&self) -> Result<(), DbErr> { + self.load_rbac_from(self) + } + + /// Load RBAC data from the given database connection and setup RBAC engine. + /// This could be from another database. + pub fn load_rbac_from(&self, db: &DbConn) -> Result<(), DbErr> { + let engine = crate::rbac::RbacEngine::load_from(db)?; + self.rbac.replace(engine); + Ok(()) + } + + /// Replace the internal RBAC engine. + pub fn replace_rbac(&self, engine: crate::rbac::RbacEngine) { + self.rbac.replace(engine); + } + + /// Create a restricted connection with access control specific for the user. + pub fn restricted_for( + &self, + user_id: crate::rbac::RbacUserId, + ) -> Result { + if self.rbac.is_some() { + Ok(crate::RestrictedConnection { + user_id, + conn: self.clone(), + }) + } else { + Err(DbErr::RbacError("engine not set up".into())) + } + } +} + +impl DatabaseConnection { + /// Get the database backend for this connection + /// + /// # Panics + /// + /// Panics if [DatabaseConnection] is `Disconnected`. + pub fn get_database_backend(&self) -> DbBackend { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(_) => DbBackend::MySql, + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(_) => DbBackend::Postgres, + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(_) => DbBackend::Sqlite, + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(_) => DbBackend::Sqlite, + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => conn.get_database_backend(), + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => conn.get_database_backend(), + DatabaseConnectionType::Disconnected => panic!("Disconnected"), + } + } + + /// Creates a [`SchemaBuilder`] for this backend + pub fn get_schema_builder(&self) -> SchemaBuilder { + Schema::new(self.get_database_backend()).builder() + } + + #[cfg(feature = "entity-registry")] + #[cfg_attr(docsrs, doc(cfg(feature = "entity-registry")))] + /// Builds a schema for all the entites in the given module + pub fn get_schema_registry(&self, prefix: &str) -> SchemaBuilder { + let schema = Schema::new(self.get_database_backend()); + crate::EntityRegistry::build_schema(schema, prefix) + } + + /// Sets a callback to metric this connection + pub fn set_metric_callback(&mut self, _callback: F) + where + F: Fn(&crate::metric::Info<'_>) + 'static, + { + match &mut self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => { + conn.set_metric_callback(_callback) + } + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => { + conn.set_metric_callback(_callback) + } + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => { + conn.set_metric_callback(_callback) + } + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => { + conn.set_metric_callback(_callback) + } + _ => {} + } + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.ping(), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => conn.ping(), + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.ping(), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.ping(), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(conn) => conn.ping(), + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(conn) => conn.ping(), + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } + + /// Explicitly close the database connection. + /// See [`Self::close_by_ref`] for usage with references. + pub fn close(self) -> Result<(), DbErr> { + self.close_by_ref() + } + + /// Explicitly close the database connection + pub fn close_by_ref(&self) -> Result<(), DbErr> { + match &self.inner { + #[cfg(feature = "sqlx-mysql")] + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => conn.close_by_ref(), + #[cfg(feature = "sqlx-postgres")] + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => conn.close_by_ref(), + #[cfg(feature = "sqlx-sqlite")] + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => conn.close_by_ref(), + #[cfg(feature = "rusqlite")] + DatabaseConnectionType::RusqliteSharedConnection(conn) => conn.close_by_ref(), + #[cfg(feature = "mock")] + DatabaseConnectionType::MockDatabaseConnection(_) => { + // Nothing to cleanup, we just consume the `DatabaseConnection` + Ok(()) + } + #[cfg(feature = "proxy")] + DatabaseConnectionType::ProxyDatabaseConnection(_) => { + // Nothing to cleanup, we just consume the `DatabaseConnection` + Ok(()) + } + DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")), + } + } +} + +impl DatabaseConnection { + /// Get [sqlx::MySqlPool] + /// + /// # Panics + /// + /// Panics if [DbConn] is not a MySQL connection. + #[cfg(feature = "sqlx-mysql")] + pub fn get_mysql_connection_pool(&self) -> &sqlx::MySqlPool { + match &self.inner { + DatabaseConnectionType::SqlxMySqlPoolConnection(conn) => &conn.pool, + _ => panic!("Not MySQL Connection"), + } + } + + /// Get [sqlx::PgPool] + /// + /// # Panics + /// + /// Panics if [DbConn] is not a Postgres connection. + #[cfg(feature = "sqlx-postgres")] + pub fn get_postgres_connection_pool(&self) -> &sqlx::PgPool { + match &self.inner { + DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => &conn.pool, + _ => panic!("Not Postgres Connection"), + } + } + + /// Get [sqlx::SqlitePool] + /// + /// # Panics + /// + /// Panics if [DbConn] is not a SQLite connection. + #[cfg(feature = "sqlx-sqlite")] + pub fn get_sqlite_connection_pool(&self) -> &sqlx::SqlitePool { + match &self.inner { + DatabaseConnectionType::SqlxSqlitePoolConnection(conn) => &conn.pool, + _ => panic!("Not SQLite Connection"), + } + } +} + +impl DbBackend { + /// Check if the URI is the same as the specified database backend. + /// Returns true if they match. + /// + /// # Panics + /// + /// Panics if `base_url` cannot be parsed as `Url`. + pub fn is_prefix_of(self, base_url: &str) -> bool { + let base_url_parsed = Url::parse(base_url).expect("Fail to parse database URL"); + match self { + Self::Postgres => { + base_url_parsed.scheme() == "postgres" || base_url_parsed.scheme() == "postgresql" + } + Self::MySql => base_url_parsed.scheme() == "mysql", + Self::Sqlite => base_url_parsed.scheme() == "sqlite", + } + } + + /// Build an SQL [Statement] + pub fn build(&self, statement: &S) -> Statement + where + S: StatementBuilder, + { + statement.build(self) + } + + /// Check if the database supports `RETURNING` syntax on insert and update + pub fn support_returning(&self) -> bool { + match self { + Self::Postgres => true, + Self::Sqlite if cfg!(feature = "sqlite-use-returning-for-3_35") => true, + Self::MySql if cfg!(feature = "mariadb-use-returning") => true, + _ => false, + } + } + + /// A getter for database dependent boolean value + pub fn boolean_value(&self, boolean: bool) -> sea_query::Value { + match self { + Self::MySql | Self::Postgres | Self::Sqlite => boolean.into(), + } + } + + /// Get the display string for this enum + pub fn as_str(&self) -> &'static str { + match self { + DatabaseBackend::MySql => "MySql", + DatabaseBackend::Postgres => "Postgres", + DatabaseBackend::Sqlite => "Sqlite", + } + } +} + +#[cfg(test)] +mod tests { + use crate::DatabaseConnection; + + #[cfg(not(feature = "sync"))] + #[test] + fn assert_database_connection_traits() { + fn assert_send_sync() {} + + assert_send_sync::(); + } +} diff --git a/sea-orm-sync/src/database/executor.rs b/sea-orm-sync/src/database/executor.rs new file mode 100644 index 0000000000..f23f3dd6ee --- /dev/null +++ b/sea-orm-sync/src/database/executor.rs @@ -0,0 +1,206 @@ +use crate::{ + AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, + ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionOptions, + TransactionTrait, +}; +use crate::{Schema, SchemaBuilder}; +use std::future::Future; +use std::pin::Pin; + +/// A wrapper that holds either a reference to a [`DatabaseConnection`] or [`DatabaseTransaction`], +/// or an owned [`DatabaseTransaction`]. +#[derive(Debug)] +pub enum DatabaseExecutor<'c> { + /// A reference to a database connection + Connection(&'c DatabaseConnection), + /// A reference to a database transaction + Transaction(&'c DatabaseTransaction), + /// An owned database transaction (used by migration's `SchemaManager::begin()`) + OwnedTransaction(DatabaseTransaction), +} + +impl<'c> From<&'c DatabaseConnection> for DatabaseExecutor<'c> { + fn from(conn: &'c DatabaseConnection) -> Self { + Self::Connection(conn) + } +} + +impl<'c> From<&'c DatabaseTransaction> for DatabaseExecutor<'c> { + fn from(trans: &'c DatabaseTransaction) -> Self { + Self::Transaction(trans) + } +} + +impl ConnectionTrait for DatabaseExecutor<'_> { + fn get_database_backend(&self) -> DbBackend { + match self { + DatabaseExecutor::Connection(conn) => conn.get_database_backend(), + DatabaseExecutor::Transaction(trans) => trans.get_database_backend(), + DatabaseExecutor::OwnedTransaction(trans) => trans.get_database_backend(), + } + } + + fn execute_raw(&self, stmt: Statement) -> Result { + match self { + DatabaseExecutor::Connection(conn) => conn.execute_raw(stmt), + DatabaseExecutor::Transaction(trans) => trans.execute_raw(stmt), + DatabaseExecutor::OwnedTransaction(trans) => trans.execute_raw(stmt), + } + } + + fn execute_unprepared(&self, sql: &str) -> Result { + match self { + DatabaseExecutor::Connection(conn) => conn.execute_unprepared(sql), + DatabaseExecutor::Transaction(trans) => trans.execute_unprepared(sql), + DatabaseExecutor::OwnedTransaction(trans) => trans.execute_unprepared(sql), + } + } + + fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { + match self { + DatabaseExecutor::Connection(conn) => conn.query_one_raw(stmt), + DatabaseExecutor::Transaction(trans) => trans.query_one_raw(stmt), + DatabaseExecutor::OwnedTransaction(trans) => trans.query_one_raw(stmt), + } + } + + fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { + match self { + DatabaseExecutor::Connection(conn) => conn.query_all_raw(stmt), + DatabaseExecutor::Transaction(trans) => trans.query_all_raw(stmt), + DatabaseExecutor::OwnedTransaction(trans) => trans.query_all_raw(stmt), + } + } +} + +impl TransactionTrait for DatabaseExecutor<'_> { + type Transaction = DatabaseTransaction; + + fn begin(&self) -> Result { + match self { + DatabaseExecutor::Connection(conn) => conn.begin(), + DatabaseExecutor::Transaction(trans) => trans.begin(), + DatabaseExecutor::OwnedTransaction(trans) => trans.begin(), + } + } + + fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + match self { + DatabaseExecutor::Connection(conn) => { + conn.begin_with_config(isolation_level, access_mode) + } + DatabaseExecutor::Transaction(trans) => { + trans.begin_with_config(isolation_level, access_mode) + } + DatabaseExecutor::OwnedTransaction(trans) => { + trans.begin_with_config(isolation_level, access_mode) + } + } + } + + fn begin_with_options( + &self, + options: TransactionOptions, + ) -> Result { + match self { + DatabaseExecutor::Connection(conn) => conn.begin_with_options(options), + DatabaseExecutor::Transaction(trans) => trans.begin_with_options(options), + DatabaseExecutor::OwnedTransaction(trans) => trans.begin_with_options(options), + } + } + + fn transaction(&self, callback: F) -> Result> + where + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + match self { + DatabaseExecutor::Connection(conn) => conn.transaction(callback), + DatabaseExecutor::Transaction(trans) => trans.transaction(callback), + DatabaseExecutor::OwnedTransaction(trans) => trans.transaction(callback), + } + } + + fn transaction_with_config( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + match self { + DatabaseExecutor::Connection(conn) => { + conn.transaction_with_config(callback, isolation_level, access_mode) + } + DatabaseExecutor::Transaction(trans) => { + trans.transaction_with_config(callback, isolation_level, access_mode) + } + DatabaseExecutor::OwnedTransaction(trans) => { + trans.transaction_with_config(callback, isolation_level, access_mode) + } + } + } +} + +/// A trait for converting into [`DatabaseExecutor`] +pub trait IntoDatabaseExecutor<'c> +where + Self: 'c, +{ + /// Convert into a [`DatabaseExecutor`] + fn into_database_executor(self) -> DatabaseExecutor<'c>; +} + +impl<'c> IntoDatabaseExecutor<'c> for DatabaseExecutor<'c> { + fn into_database_executor(self) -> DatabaseExecutor<'c> { + self + } +} + +impl<'c> IntoDatabaseExecutor<'c> for &'c DatabaseConnection { + fn into_database_executor(self) -> DatabaseExecutor<'c> { + DatabaseExecutor::Connection(self) + } +} + +impl<'c> IntoDatabaseExecutor<'c> for &'c DatabaseTransaction { + fn into_database_executor(self) -> DatabaseExecutor<'c> { + DatabaseExecutor::Transaction(self) + } +} + +impl IntoDatabaseExecutor<'static> for DatabaseTransaction { + fn into_database_executor(self) -> DatabaseExecutor<'static> { + DatabaseExecutor::OwnedTransaction(self) + } +} + +impl DatabaseExecutor<'_> { + /// Returns `true` if this executor is backed by a transaction (borrowed or owned). + pub fn is_transaction(&self) -> bool { + matches!( + self, + DatabaseExecutor::Transaction(_) | DatabaseExecutor::OwnedTransaction(_) + ) + } + + /// Creates a [`SchemaBuilder`] for this backend + pub fn get_schema_builder(&self) -> SchemaBuilder { + Schema::new(self.get_database_backend()).builder() + } + + #[cfg(feature = "entity-registry")] + #[cfg_attr(docsrs, doc(cfg(feature = "entity-registry")))] + /// Builds a schema for all the entities in the given module + pub fn get_schema_registry(&self, prefix: &str) -> SchemaBuilder { + let schema = Schema::new(self.get_database_backend()); + crate::EntityRegistry::build_schema(schema, prefix) + } +} diff --git a/sea-orm-sync/src/database/mock.rs b/sea-orm-sync/src/database/mock.rs new file mode 100644 index 0000000000..279b517739 --- /dev/null +++ b/sea-orm-sync/src/database/mock.rs @@ -0,0 +1,869 @@ +use crate::{ + DatabaseConnection, DatabaseConnectionType, DbBackend, EntityTrait, ExecResult, + ExecResultHolder, Iden, IdenStatic, Iterable, MockDatabaseConnection, MockDatabaseTrait, + ModelTrait, QueryResult, QueryResultRow, SelectA, SelectB, Statement, error::*, +}; +use sea_query::{Value, ValueType, Values}; +use std::{collections::BTreeMap, sync::Arc}; +use tracing::instrument; + +/// Defines a Mock database suitable for testing +#[derive(Debug)] +pub struct MockDatabase { + db_backend: DbBackend, + transaction: Option, + transaction_log: Vec, + exec_results: Vec>, + query_results: Vec, DbErr>>, +} + +/// Defines the results obtained from a [MockDatabase] +#[derive(Clone, Debug, Default)] +pub struct MockExecResult { + /// The last inserted id on auto-increment + pub last_insert_id: u64, + /// The number of rows affected by the database operation + pub rows_affected: u64, +} + +/// Defines the structure of a test Row for the [MockDatabase] +/// which is just a [BTreeMap]<[String], [Value]> +#[derive(Clone, Debug)] +pub struct MockRow { + /// The values of the single row + pub(crate) values: BTreeMap, +} + +/// A trait to get a [MockRow] from a type useful for testing in the [MockDatabase] +pub trait IntoMockRow { + /// The method to perform this operation + fn into_mock_row(self) -> MockRow; +} + +/// Defines a transaction that is has not been committed +#[derive(Debug)] +pub struct OpenTransaction { + stmts: Vec, + transaction_depth: usize, +} + +/// Defines a database transaction as it holds a Vec<[Statement]> +#[derive(Debug, Clone, PartialEq)] +pub struct Transaction { + stmts: Vec, +} + +impl MockDatabase { + /// Instantiate a mock database with a [DbBackend] to simulate real + /// world SQL databases + pub fn new(db_backend: DbBackend) -> Self { + Self { + db_backend, + transaction: None, + transaction_log: Vec::new(), + exec_results: Vec::new(), + query_results: Vec::new(), + } + } + + /// Create a database connection + pub fn into_connection(self) -> DatabaseConnection { + DatabaseConnectionType::MockDatabaseConnection(Arc::new(MockDatabaseConnection::new(self))) + .into() + } + + /// Add some [MockExecResult]s to `exec_results` + pub fn append_exec_results(mut self, vec: I) -> Self + where + I: IntoIterator, + { + self.exec_results.extend(vec.into_iter().map(Result::Ok)); + self + } + + /// Add some Values to `query_results` + pub fn append_query_results(mut self, vec: II) -> Self + where + T: IntoMockRow, + I: IntoIterator, + II: IntoIterator, + { + for row in vec.into_iter() { + let row = row.into_iter().map(|vec| Ok(vec.into_mock_row())).collect(); + self.query_results.push(row); + } + self + } + + /// Add some [DbErr]s to `exec_results` + pub fn append_exec_errors(mut self, vec: I) -> Self + where + I: IntoIterator, + { + self.exec_results.extend(vec.into_iter().map(Result::Err)); + self + } + + /// Add some [DbErr]s to `query_results` + pub fn append_query_errors(mut self, vec: I) -> Self + where + I: IntoIterator, + { + self.query_results.extend(vec.into_iter().map(Result::Err)); + self + } +} + +impl MockDatabaseTrait for MockDatabase { + #[instrument(level = "trace")] + fn execute(&mut self, counter: usize, statement: Statement) -> Result { + if let Some(transaction) = &mut self.transaction { + transaction.push(statement); + } else { + self.transaction_log.push(Transaction::one(statement)); + } + if counter < self.exec_results.len() { + match std::mem::replace( + &mut self.exec_results[counter], + Err(exec_err("this value has been consumed already")), + ) { + Ok(result) => Ok(ExecResult { + result: ExecResultHolder::Mock(result), + }), + Err(err) => Err(err), + } + } else { + Err(exec_err("`exec_results` buffer is empty")) + } + } + + #[instrument(level = "trace")] + fn query(&mut self, counter: usize, statement: Statement) -> Result, DbErr> { + if let Some(transaction) = &mut self.transaction { + transaction.push(statement); + } else { + self.transaction_log.push(Transaction::one(statement)); + } + if counter < self.query_results.len() { + match std::mem::replace( + &mut self.query_results[counter], + Err(query_err("this value has been consumed already")), + ) { + Ok(result) => Ok(result + .into_iter() + .map(|row| QueryResult { + row: QueryResultRow::Mock(row), + }) + .collect()), + Err(err) => Err(err), + } + } else { + Err(query_err("`query_results` buffer is empty.")) + } + } + + #[instrument(level = "trace")] + fn begin(&mut self) { + match self.transaction.as_mut() { + Some(transaction) => transaction.begin_nested(self.db_backend), + None => self.transaction = Some(OpenTransaction::init()), + } + } + + #[instrument(level = "trace")] + fn commit(&mut self) { + match self.transaction.as_mut() { + Some(transaction) => { + if transaction.commit(self.db_backend) { + if let Some(transaction) = self.transaction.take() { + self.transaction_log.push(transaction.into_transaction()); + } + } + } + None => panic!("There is no open transaction to commit"), + } + } + + #[instrument(level = "trace")] + fn rollback(&mut self) { + match self.transaction.as_mut() { + Some(transaction) => { + if transaction.rollback(self.db_backend) { + if let Some(transaction) = self.transaction.take() { + self.transaction_log.push(transaction.into_transaction()); + } + } + } + None => panic!("There is no open transaction to rollback"), + } + } + + fn drain_transaction_log(&mut self) -> Vec { + std::mem::take(&mut self.transaction_log) + } + + fn get_database_backend(&self) -> DbBackend { + self.db_backend + } + + fn ping(&self) -> Result<(), DbErr> { + Ok(()) + } +} + +impl MockRow { + /// Get a value from the [MockRow] + pub fn try_get(&self, index: I) -> Result + where + T: ValueType, + { + if let Some(index) = index.as_str() { + T::try_from( + self.values + .get(index) + .ok_or_else(|| query_err(format!("No column for ColIdx {index:?}")))? + .clone(), + ) + .map_err(type_err) + } else if let Some(index) = index.as_usize() { + let (_, value) = self + .values + .iter() + .nth(*index) + .ok_or_else(|| query_err(format!("Column at index {index} not found")))?; + T::try_from(value.clone()).map_err(type_err) + } else { + unreachable!("Missing ColIdx implementation for MockRow"); + } + } + + /// An iterator over the keys and values of a mock row + pub fn into_column_value_tuples(self) -> impl Iterator { + self.values.into_iter() + } +} + +impl IntoMockRow for MockRow { + fn into_mock_row(self) -> MockRow { + self + } +} + +impl IntoMockRow for M +where + M: ModelTrait, +{ + fn into_mock_row(self) -> MockRow { + let mut values = BTreeMap::new(); + for col in <::Column>::iter() { + values.insert(col.to_string(), self.get(col)); + } + MockRow { values } + } +} + +impl IntoMockRow for (M, N) +where + M: ModelTrait, + N: ModelTrait, +{ + fn into_mock_row(self) -> MockRow { + let mut mapped_join = BTreeMap::new(); + + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectA.as_str(), column.as_str()), + self.0.get(column), + ); + } + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectB.as_str(), column.as_str()), + self.1.get(column), + ); + } + + mapped_join.into_mock_row() + } +} + +impl IntoMockRow for (M, Option) +where + M: ModelTrait, + N: ModelTrait, +{ + fn into_mock_row(self) -> MockRow { + let mut mapped_join = BTreeMap::new(); + + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectA.as_str(), column.as_str()), + self.0.get(column), + ); + } + if let Some(b_entity) = self.1 { + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectB.as_str(), column.as_str()), + b_entity.get(column), + ); + } + } + + mapped_join.into_mock_row() + } +} + +impl IntoMockRow for BTreeMap +where + T: Into, +{ + fn into_mock_row(self) -> MockRow { + MockRow { + values: self.into_iter().map(|(k, v)| (k.into(), v)).collect(), + } + } +} + +impl Transaction { + /// Get the [Value]s from s raw SQL statement depending on the [DatabaseBackend](crate::DatabaseBackend) + pub fn from_sql_and_values(db_backend: DbBackend, sql: T, values: I) -> Self + where + I: IntoIterator, + T: Into, + { + Self::one(Statement::from_string_values_tuple( + db_backend, + (sql, Values(values.into_iter().collect())), + )) + } + + /// Create a Transaction with one statement + pub fn one(stmt: Statement) -> Self { + Self { stmts: vec![stmt] } + } + + /// Create a Transaction with many statements + pub fn many(stmts: I) -> Self + where + I: IntoIterator, + { + Self { + stmts: stmts.into_iter().collect(), + } + } + + /// Wrap each Statement as a single-statement Transaction + pub fn wrap(stmts: I) -> Vec + where + I: IntoIterator, + { + stmts.into_iter().map(Self::one).collect() + } + + /// Get the list of statements + pub fn statements(&self) -> &[Statement] { + &self.stmts + } +} + +impl OpenTransaction { + fn init() -> Self { + Self { + stmts: vec![Statement::from_string(DbBackend::Postgres, "BEGIN")], + transaction_depth: 0, + } + } + + fn begin_nested(&mut self, db_backend: DbBackend) { + self.transaction_depth += 1; + self.push(Statement::from_string( + db_backend, + format!("SAVEPOINT savepoint_{}", self.transaction_depth), + )); + } + + fn commit(&mut self, db_backend: DbBackend) -> bool { + if self.transaction_depth == 0 { + self.push(Statement::from_string(db_backend, "COMMIT")); + true + } else { + self.push(Statement::from_string( + db_backend, + format!("RELEASE SAVEPOINT savepoint_{}", self.transaction_depth), + )); + self.transaction_depth -= 1; + false + } + } + + fn rollback(&mut self, db_backend: DbBackend) -> bool { + if self.transaction_depth == 0 { + self.push(Statement::from_string(db_backend, "ROLLBACK")); + true + } else { + self.push(Statement::from_string( + db_backend, + format!("ROLLBACK TO SAVEPOINT savepoint_{}", self.transaction_depth), + )); + self.transaction_depth -= 1; + false + } + } + + fn push(&mut self, stmt: Statement) { + self.stmts.push(stmt); + } + + fn into_transaction(self) -> Transaction { + match self.transaction_depth { + 0 => Transaction { stmts: self.stmts }, + _ => panic!("There is uncommitted nested transaction"), + } + } +} + +#[cfg(test)] +#[cfg(feature = "mock")] +mod tests { + #[cfg(feature = "sync")] + use crate::util::StreamShim; + use crate::{ + DbBackend, DbErr, IntoMockRow, MockDatabase, Statement, Transaction, TransactionError, + TransactionTrait, entity::*, error::*, tests_cfg::*, + }; + use pretty_assertions::assert_eq; + + #[derive(Debug, PartialEq, Eq)] + pub struct MyErr(String); + + impl std::error::Error for MyErr {} + + impl std::fmt::Display for MyErr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } + } + + #[test] + fn test_transaction_1() { + let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + + db.transaction::<_, (), DbErr>(|txn| { + ({ + let _1 = cake::Entity::find().one(txn); + let _2 = fruit::Entity::find().all(txn); + + Ok(()) + }) + }) + .unwrap(); + + let _ = cake::Entity::find().all(&db); + + assert_eq!( + db.into_transaction_log(), + [ + Transaction::many([ + Statement::from_string(DbBackend::Postgres, "BEGIN"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, + [1u64.into()] + ), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit""#, + [] + ), + Statement::from_string(DbBackend::Postgres, "COMMIT"), + ]), + Transaction::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + [] + ), + ] + ); + } + + #[test] + fn test_transaction_2() { + let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + + let result = db.transaction::<_, (), MyErr>(|txn| { + ({ + let _ = cake::Entity::find().one(txn); + Err(MyErr("test".to_owned())) + }) + }); + + match result { + Err(TransactionError::Transaction(err)) => { + assert_eq!(err, MyErr("test".to_owned())) + } + _ => unreachable!(), + } + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([ + Statement::from_string(DbBackend::Postgres, "BEGIN"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, + [1u64.into()] + ), + Statement::from_string(DbBackend::Postgres, "ROLLBACK"), + ])] + ); + } + + #[test] + fn test_nested_transaction_1() { + let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + + db.transaction::<_, (), DbErr>(|txn| { + ({ + let _ = cake::Entity::find().one(txn); + + txn.transaction::<_, (), DbErr>(|txn| { + ({ + let _ = fruit::Entity::find().all(txn); + + Ok(()) + }) + }) + .unwrap(); + + Ok(()) + }) + }) + .unwrap(); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([ + Statement::from_string(DbBackend::Postgres, "BEGIN"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, + [1u64.into()] + ), + Statement::from_string(DbBackend::Postgres, "SAVEPOINT savepoint_1"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit""#, + [] + ), + Statement::from_string(DbBackend::Postgres, "RELEASE SAVEPOINT savepoint_1"), + Statement::from_string(DbBackend::Postgres, "COMMIT"), + ]),] + ); + } + + #[test] + fn test_nested_transaction_2() { + let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + + db.transaction::<_, (), DbErr>(|txn| { + ({ + let _ = cake::Entity::find().one(txn); + + txn.transaction::<_, (), DbErr>(|txn| { + ({ + let _ = fruit::Entity::find().all(txn); + + txn.transaction::<_, (), DbErr>(|txn| { + ({ + let _ = cake::Entity::find().all(txn); + + Ok(()) + }) + }) + .unwrap(); + + Ok(()) + }) + }) + .unwrap(); + + Ok(()) + }) + }) + .unwrap(); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([ + Statement::from_string(DbBackend::Postgres, "BEGIN"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, + [1u64.into()] + ), + Statement::from_string(DbBackend::Postgres, "SAVEPOINT savepoint_1"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit""#, + [] + ), + Statement::from_string(DbBackend::Postgres, "SAVEPOINT savepoint_2"), + Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + [] + ), + Statement::from_string(DbBackend::Postgres, "RELEASE SAVEPOINT savepoint_2"), + Statement::from_string(DbBackend::Postgres, "RELEASE SAVEPOINT savepoint_1"), + Statement::from_string(DbBackend::Postgres, "COMMIT"), + ]),] + ); + } + + #[test] + fn test_stream_1() -> Result<(), DbErr> { + let apple = fruit::Model { + id: 1, + name: "Apple".to_owned(), + cake_id: Some(1), + }; + + let orange = fruit::Model { + id: 2, + name: "orange".to_owned(), + cake_id: None, + }; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[apple.clone(), orange.clone()]]) + .into_connection(); + + let mut stream = fruit::Entity::find().stream(&db)?; + + assert_eq!(stream.try_next()?, Some(apple)); + + assert_eq!(stream.try_next()?, Some(orange)); + + assert_eq!(stream.try_next()?, None); + + Ok(()) + } + + #[test] + fn test_stream_2() -> Result<(), DbErr> { + use fruit::Entity as Fruit; + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([Vec::::new()]) + .into_connection(); + + let mut stream = Fruit::find().stream(&db)?; + + while let Some(item) = stream.try_next()? { + let _item: fruit::ActiveModel = item.into(); + } + + Ok(()) + } + + #[test] + fn test_stream_in_transaction() -> Result<(), DbErr> { + let apple = fruit::Model { + id: 1, + name: "Apple".to_owned(), + cake_id: Some(1), + }; + + let orange = fruit::Model { + id: 2, + name: "orange".to_owned(), + cake_id: None, + }; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[apple.clone(), orange.clone()]]) + .into_connection(); + + let txn = db.begin()?; + + if let Ok(mut stream) = fruit::Entity::find().stream(&txn) { + assert_eq!(stream.try_next()?, Some(apple)); + + assert_eq!(stream.try_next()?, Some(orange)); + + assert_eq!(stream.try_next()?, None); + + // stream will be dropped end of scope + } + + txn.commit()?; + + Ok(()) + } + + #[test] + fn test_mocked_join() { + let row = ( + cake::Model { + id: 1, + name: "Apple Cake".to_owned(), + }, + fruit::Model { + id: 2, + name: "Apple".to_owned(), + cake_id: Some(1), + }, + ); + let mocked_row = row.into_mock_row(); + + let a_id = mocked_row.try_get::("A_id"); + assert!(a_id.is_ok()); + assert_eq!(1, a_id.unwrap()); + let b_id = mocked_row.try_get::("B_id"); + assert!(b_id.is_ok()); + assert_eq!(2, b_id.unwrap()); + } + + #[test] + fn test_find_also_related_1() -> Result<(), DbErr> { + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[( + cake::Model { + id: 1, + name: "Apple Cake".to_owned(), + }, + fruit::Model { + id: 2, + name: "Apple".to_owned(), + cake_id: Some(1), + }, + )]]) + .into_connection(); + + assert_eq!( + cake::Entity::find() + .find_also_related(fruit::Entity) + .all(&db)?, + [( + cake::Model { + id: 1, + name: "Apple Cake".to_owned(), + }, + Some(fruit::Model { + id: 2, + name: "Apple".to_owned(), + cake_id: Some(1), + }) + )] + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name", "fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id" FROM "cake" LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + [] + ),] + ); + + Ok(()) + } + + #[cfg(feature = "postgres-array")] + #[test] + fn test_postgres_array_1() -> Result<(), DbErr> { + mod collection { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "collection")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub integers: Vec, + pub integers_opt: Option>, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[ + collection::Model { + id: 1, + integers: vec![1, 2, 3], + integers_opt: Some(vec![1, 2, 3]), + }, + collection::Model { + id: 2, + integers: vec![], + integers_opt: Some(vec![]), + }, + collection::Model { + id: 3, + integers: vec![3, 1, 4], + integers_opt: None, + }, + ]]) + .into_connection(); + + assert_eq!( + collection::Entity::find().all(&db)?, + [ + collection::Model { + id: 1, + integers: vec![1, 2, 3], + integers_opt: Some(vec![1, 2, 3]), + }, + collection::Model { + id: 2, + integers: vec![], + integers_opt: Some(vec![]), + }, + collection::Model { + id: 3, + integers: vec![3, 1, 4], + integers_opt: None, + }, + ] + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "collection"."id", "collection"."integers", "collection"."integers_opt" FROM "collection""#, + [] + ),] + ); + + Ok(()) + } + + #[test] + fn test_query_err() { + let db = MockDatabase::new(DbBackend::MySql) + .append_query_errors([query_err("this is a mock query error")]) + .into_connection(); + + assert_eq!( + cake::Entity::find().all(&db), + Err(query_err("this is a mock query error")) + ); + } + + #[test] + fn test_exec_err() { + let db = MockDatabase::new(DbBackend::MySql) + .append_exec_errors([exec_err("this is a mock exec error")]) + .into_connection(); + + let model = cake::ActiveModel::new(); + + assert_eq!(model.save(&db), Err(exec_err("this is a mock exec error"))); + } +} diff --git a/sea-orm-sync/src/database/mod.rs b/sea-orm-sync/src/database/mod.rs new file mode 100644 index 0000000000..970114fb3c --- /dev/null +++ b/sea-orm-sync/src/database/mod.rs @@ -0,0 +1,457 @@ +use std::{sync::Arc, time::Duration}; + +#[cfg(feature = "sqlx-mysql")] +use sqlx::mysql::MySqlConnectOptions; +#[cfg(feature = "sqlx-postgres")] +use sqlx::postgres::PgConnectOptions; +#[cfg(feature = "sqlx-sqlite")] +use sqlx::sqlite::SqliteConnectOptions; + +mod connection; +mod db_connection; +mod executor; +#[cfg(feature = "mock")] +#[cfg_attr(docsrs, doc(cfg(feature = "mock")))] +mod mock; +#[cfg(feature = "proxy")] +#[cfg_attr(docsrs, doc(cfg(feature = "proxy")))] +mod proxy; +#[cfg(feature = "rbac")] +mod restricted_connection; +#[cfg(all(feature = "schema-sync", feature = "rusqlite"))] +mod sea_schema_rusqlite; +#[cfg(all(feature = "schema-sync", feature = "sqlx-dep"))] +mod sea_schema_shim; +mod statement; +mod stream; +mod tracing_spans; +mod transaction; + +pub use connection::*; +pub use db_connection::*; +pub use executor::*; +#[cfg(feature = "mock")] +#[cfg_attr(docsrs, doc(cfg(feature = "mock")))] +pub use mock::*; +#[cfg(feature = "proxy")] +#[cfg_attr(docsrs, doc(cfg(feature = "proxy")))] +pub use proxy::*; +#[cfg(feature = "rbac")] +pub use restricted_connection::*; +pub use statement::*; +use std::borrow::Cow; +pub use stream::*; +use tracing::instrument; +pub use transaction::*; + +use crate::error::*; + +/// Defines a database +#[derive(Debug, Default)] +pub struct Database; + +#[cfg(feature = "sync")] +type BoxFuture<'a, T> = T; + +type AfterConnectCallback = + Option BoxFuture<'static, Result<(), DbErr>> + 'static>>; + +/// Defines the configuration options of a database +#[derive(derive_more::Debug, Clone)] +pub struct ConnectOptions { + /// The URI of the database + pub(crate) url: String, + /// Maximum number of connections for a pool + pub(crate) max_connections: Option, + /// Minimum number of connections for a pool + pub(crate) min_connections: Option, + /// The connection timeout for a packet connection + pub(crate) connect_timeout: Option, + /// Maximum idle time for a particular connection to prevent + /// network resource exhaustion + pub(crate) idle_timeout: Option>, + /// Set the maximum amount of time to spend waiting for acquiring a connection + pub(crate) acquire_timeout: Option, + /// Set the maximum lifetime of individual connections + pub(crate) max_lifetime: Option>, + /// Enable SQLx statement logging + pub(crate) sqlx_logging: bool, + /// SQLx statement logging level (ignored if `sqlx_logging` is false) + pub(crate) sqlx_logging_level: log::LevelFilter, + /// SQLx slow statements logging level (ignored if `sqlx_logging` is false) + pub(crate) sqlx_slow_statements_logging_level: log::LevelFilter, + /// SQLx slow statements duration threshold (ignored if `sqlx_logging` is false) + pub(crate) sqlx_slow_statements_logging_threshold: Duration, + /// set sqlcipher key + pub(crate) sqlcipher_key: Option>, + /// Schema search path (PostgreSQL only) + pub(crate) schema_search_path: Option, + /// Application name (PostgreSQL only) + pub(crate) application_name: Option, + /// Statement timeout (PostgreSQL only) + pub(crate) statement_timeout: Option, + pub(crate) test_before_acquire: bool, + /// Only establish connections to the DB as needed. If set to `true`, the db connection will + /// be created using SQLx's [connect_lazy](https://docs.rs/sqlx/latest/sqlx/struct.Pool.html#method.connect_lazy) + /// method. + pub(crate) connect_lazy: bool, + + #[debug(skip)] + pub(crate) after_connect: AfterConnectCallback, + + #[cfg(feature = "sqlx-mysql")] + #[debug(skip)] + pub(crate) mysql_opts_fn: Option MySqlConnectOptions>>, + #[cfg(feature = "sqlx-postgres")] + #[debug(skip)] + pub(crate) pg_opts_fn: Option PgConnectOptions>>, + #[cfg(feature = "sqlx-sqlite")] + #[debug(skip)] + pub(crate) sqlite_opts_fn: Option SqliteConnectOptions>>, +} + +impl Database { + /// Method to create a [DatabaseConnection] on a database. This method will return an error + /// if the database is not available. + #[instrument(level = "trace", skip(opt))] + pub fn connect(opt: C) -> Result + where + C: Into, + { + let opt: ConnectOptions = opt.into(); + + if url::Url::parse(&opt.url).is_err() { + return Err(conn_err(format!( + "The connection string '{}' cannot be parsed.", + opt.url + ))); + } + + #[cfg(feature = "sqlx-mysql")] + if DbBackend::MySql.is_prefix_of(&opt.url) { + return crate::SqlxMySqlConnector::connect(opt); + } + #[cfg(feature = "sqlx-postgres")] + if DbBackend::Postgres.is_prefix_of(&opt.url) { + return crate::SqlxPostgresConnector::connect(opt); + } + #[cfg(feature = "sqlx-sqlite")] + if DbBackend::Sqlite.is_prefix_of(&opt.url) { + return crate::SqlxSqliteConnector::connect(opt); + } + #[cfg(feature = "rusqlite")] + if DbBackend::Sqlite.is_prefix_of(&opt.url) { + return crate::driver::rusqlite::RusqliteConnector::connect(opt); + } + #[cfg(feature = "mock")] + if crate::MockDatabaseConnector::accepts(&opt.url) { + return crate::MockDatabaseConnector::connect(&opt.url); + } + + Err(conn_err(format!( + "The connection string '{}' has no supporting driver.", + opt.url + ))) + } + + /// Method to create a [DatabaseConnection] on a proxy database + #[cfg(feature = "proxy")] + #[instrument(level = "trace", skip(proxy_func_arc))] + pub fn connect_proxy( + db_type: DbBackend, + proxy_func_arc: std::sync::Arc>, + ) -> Result { + match db_type { + DbBackend::MySql => { + return crate::ProxyDatabaseConnector::connect( + DbBackend::MySql, + proxy_func_arc.to_owned(), + ); + } + DbBackend::Postgres => { + return crate::ProxyDatabaseConnector::connect( + DbBackend::Postgres, + proxy_func_arc.to_owned(), + ); + } + DbBackend::Sqlite => { + return crate::ProxyDatabaseConnector::connect( + DbBackend::Sqlite, + proxy_func_arc.to_owned(), + ); + } + } + } +} + +impl From for ConnectOptions +where + T: Into, +{ + fn from(s: T) -> ConnectOptions { + ConnectOptions::new(s.into()) + } +} + +impl ConnectOptions { + /// Create new [ConnectOptions] for a [Database] by passing in a URI string + pub fn new(url: T) -> Self + where + T: Into, + { + Self { + url: url.into(), + max_connections: None, + min_connections: None, + connect_timeout: None, + idle_timeout: None, + acquire_timeout: None, + max_lifetime: None, + sqlx_logging: true, + sqlx_logging_level: log::LevelFilter::Info, + sqlx_slow_statements_logging_level: log::LevelFilter::Off, + sqlx_slow_statements_logging_threshold: Duration::from_secs(1), + sqlcipher_key: None, + schema_search_path: None, + application_name: None, + statement_timeout: None, + test_before_acquire: true, + connect_lazy: false, + after_connect: None, + #[cfg(feature = "sqlx-mysql")] + mysql_opts_fn: None, + #[cfg(feature = "sqlx-postgres")] + pg_opts_fn: None, + #[cfg(feature = "sqlx-sqlite")] + sqlite_opts_fn: None, + } + } + + /// Get the database URL of the pool + pub fn get_url(&self) -> &str { + &self.url + } + + /// Set the maximum number of connections of the pool + pub fn max_connections(&mut self, value: u32) -> &mut Self { + self.max_connections = Some(value); + self + } + + /// Get the maximum number of connections of the pool, if set + pub fn get_max_connections(&self) -> Option { + self.max_connections + } + + /// Set the minimum number of connections of the pool + pub fn min_connections(&mut self, value: u32) -> &mut Self { + self.min_connections = Some(value); + self + } + + /// Get the minimum number of connections of the pool, if set + pub fn get_min_connections(&self) -> Option { + self.min_connections + } + + /// Set the timeout duration when acquiring a connection + pub fn connect_timeout(&mut self, value: Duration) -> &mut Self { + self.connect_timeout = Some(value); + self + } + + /// Get the timeout duration when acquiring a connection, if set + pub fn get_connect_timeout(&self) -> Option { + self.connect_timeout + } + + /// Set the idle duration before closing a connection. + pub fn idle_timeout(&mut self, value: T) -> &mut Self + where + T: Into>, + { + self.idle_timeout = Some(value.into()); + self + } + + /// Get the idle duration before closing a connection, if set + pub fn get_idle_timeout(&self) -> Option> { + self.idle_timeout + } + + /// Set the maximum amount of time to spend waiting for acquiring a connection + pub fn acquire_timeout(&mut self, value: Duration) -> &mut Self { + self.acquire_timeout = Some(value); + self + } + + /// Get the maximum amount of time to spend waiting for acquiring a connection + pub fn get_acquire_timeout(&self) -> Option { + self.acquire_timeout + } + + /// Set the maximum lifetime of individual connections. + pub fn max_lifetime(&mut self, lifetime: T) -> &mut Self + where + T: Into>, + { + self.max_lifetime = Some(lifetime.into()); + self + } + + /// Get the maximum lifetime of individual connections, if set + pub fn get_max_lifetime(&self) -> Option> { + self.max_lifetime + } + + /// Enable SQLx statement logging (default true) + pub fn sqlx_logging(&mut self, value: bool) -> &mut Self { + self.sqlx_logging = value; + self + } + + /// Get whether SQLx statement logging is enabled + pub fn get_sqlx_logging(&self) -> bool { + self.sqlx_logging + } + + /// Set SQLx statement logging level (default INFO). + /// (ignored if `sqlx_logging` is `false`) + pub fn sqlx_logging_level(&mut self, level: log::LevelFilter) -> &mut Self { + self.sqlx_logging_level = level; + self + } + + /// Set SQLx slow statements logging level and duration threshold (default `LevelFilter::Off`). + /// (ignored if `sqlx_logging` is `false`) + pub fn sqlx_slow_statements_logging_settings( + &mut self, + level: log::LevelFilter, + duration: Duration, + ) -> &mut Self { + self.sqlx_slow_statements_logging_level = level; + self.sqlx_slow_statements_logging_threshold = duration; + self + } + + /// Get the level of SQLx statement logging + pub fn get_sqlx_logging_level(&self) -> log::LevelFilter { + self.sqlx_logging_level + } + + /// Get the SQLx slow statements logging settings + pub fn get_sqlx_slow_statements_logging_settings(&self) -> (log::LevelFilter, Duration) { + ( + self.sqlx_slow_statements_logging_level, + self.sqlx_slow_statements_logging_threshold, + ) + } + + /// set key for sqlcipher + pub fn sqlcipher_key(&mut self, value: T) -> &mut Self + where + T: Into>, + { + self.sqlcipher_key = Some(value.into()); + self + } + + /// Set schema search path (PostgreSQL only) + pub fn set_schema_search_path(&mut self, schema_search_path: T) -> &mut Self + where + T: Into, + { + self.schema_search_path = Some(schema_search_path.into()); + self + } + + /// Set application name (PostgreSQL only) + pub fn set_application_name(&mut self, application_name: T) -> &mut Self + where + T: Into, + { + self.application_name = Some(application_name.into()); + self + } + + /// Set the statement timeout (PostgreSQL only). + /// + /// This sets the PostgreSQL `statement_timeout` parameter via the connection options, + /// causing the server to abort any statement that exceeds the specified duration. + /// The timeout is applied at connection time and does not require an extra roundtrip. + /// + /// Has no effect on MySQL or SQLite connections. + pub fn statement_timeout(&mut self, value: Duration) -> &mut Self { + self.statement_timeout = Some(value); + self + } + + /// Get the statement timeout, if set + pub fn get_statement_timeout(&self) -> Option { + self.statement_timeout + } + + /// If true, the connection will be pinged upon acquiring from the pool (default true). + pub fn test_before_acquire(&mut self, value: bool) -> &mut Self { + self.test_before_acquire = value; + self + } + + /// If set to `true`, the db connection pool will be created using SQLx's + /// [connect_lazy](https://docs.rs/sqlx/latest/sqlx/struct.Pool.html#method.connect_lazy) method. + pub fn connect_lazy(&mut self, value: bool) -> &mut Self { + self.connect_lazy = value; + self + } + + /// Get whether DB connections will be established when the pool is created or only as needed. + pub fn get_connect_lazy(&self) -> bool { + self.connect_lazy + } + + /// Set a callback function that will be called after a new connection is established. + pub fn after_connect(&mut self, f: F) -> &mut Self + where + F: Fn(DatabaseConnection) -> BoxFuture<'static, Result<(), DbErr>> + 'static, + { + self.after_connect = Some(Arc::new(f)); + + self + } + + #[cfg(feature = "sqlx-mysql")] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlx-mysql")))] + /// Apply a function to modify the underlying [`MySqlConnectOptions`] before + /// creating the connection pool. + pub fn map_sqlx_mysql_opts(&mut self, f: F) -> &mut Self + where + F: Fn(MySqlConnectOptions) -> MySqlConnectOptions + 'static, + { + self.mysql_opts_fn = Some(Arc::new(f)); + self + } + + #[cfg(feature = "sqlx-postgres")] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlx-postgres")))] + /// Apply a function to modify the underlying [`PgConnectOptions`] before + /// creating the connection pool. + pub fn map_sqlx_postgres_opts(&mut self, f: F) -> &mut Self + where + F: Fn(PgConnectOptions) -> PgConnectOptions + 'static, + { + self.pg_opts_fn = Some(Arc::new(f)); + self + } + + #[cfg(feature = "sqlx-sqlite")] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlx-sqlite")))] + /// Apply a function to modify the underlying [`SqliteConnectOptions`] before + /// creating the connection pool. + pub fn map_sqlx_sqlite_opts(&mut self, f: F) -> &mut Self + where + F: Fn(SqliteConnectOptions) -> SqliteConnectOptions + 'static, + { + self.sqlite_opts_fn = Some(Arc::new(f)); + self + } +} diff --git a/sea-orm-sync/src/database/proxy.rs b/sea-orm-sync/src/database/proxy.rs new file mode 100644 index 0000000000..cddd40f429 --- /dev/null +++ b/sea-orm-sync/src/database/proxy.rs @@ -0,0 +1,243 @@ +use crate::{ExecResult, ExecResultHolder, QueryResult, QueryResultRow, Statement, error::*}; + +use sea_query::{Value, ValueType}; +use std::{collections::BTreeMap, fmt::Debug}; + +/// Defines the [ProxyDatabaseTrait] to save the functions +pub trait ProxyDatabaseTrait: Debug { + /// Execute a query in the [ProxyDatabase], and return the query results + fn query(&self, statement: Statement) -> Result, DbErr>; + + /// Execute a command in the [ProxyDatabase], and report the number of rows affected + fn execute(&self, statement: Statement) -> Result; + + /// Begin a transaction in the [ProxyDatabase] + fn begin(&self) {} + + /// Commit a transaction in the [ProxyDatabase] + fn commit(&self) {} + + /// Rollback a transaction in the [ProxyDatabase] + fn rollback(&self) {} + + /// Start rollback a transaction in the [ProxyDatabase] + fn start_rollback(&self) {} + + /// Ping the [ProxyDatabase], it should return an error if the database is not available + fn ping(&self) -> Result<(), DbErr> { + Ok(()) + } +} + +/// Defines the results obtained from a [ProxyDatabase] +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct ProxyExecResult { + /// The last inserted id on auto-increment + pub last_insert_id: u64, + /// The number of rows affected by the database operation + pub rows_affected: u64, +} + +impl ProxyExecResult { + /// Create a new [ProxyExecResult] from the last inserted id and the number of rows affected + pub fn new(last_insert_id: u64, rows_affected: u64) -> Self { + Self { + last_insert_id, + rows_affected, + } + } +} + +impl Default for ExecResultHolder { + fn default() -> Self { + Self::Proxy(ProxyExecResult::default()) + } +} + +impl From for ExecResult { + fn from(result: ProxyExecResult) -> Self { + Self { + result: ExecResultHolder::Proxy(result), + } + } +} + +impl From for ProxyExecResult { + fn from(result: ExecResult) -> Self { + match result.result { + #[cfg(feature = "sqlx-mysql")] + ExecResultHolder::SqlxMySql(result) => Self { + last_insert_id: result.last_insert_id(), + rows_affected: result.rows_affected(), + }, + #[cfg(feature = "sqlx-postgres")] + ExecResultHolder::SqlxPostgres(result) => Self { + last_insert_id: 0, + rows_affected: result.rows_affected(), + }, + #[cfg(feature = "sqlx-sqlite")] + ExecResultHolder::SqlxSqlite(result) => Self { + last_insert_id: result.last_insert_rowid() as u64, + rows_affected: result.rows_affected(), + }, + #[cfg(feature = "mock")] + ExecResultHolder::Mock(result) => Self { + last_insert_id: result.last_insert_id, + rows_affected: result.rows_affected, + }, + ExecResultHolder::Proxy(result) => result, + } + } +} + +/// Defines the structure of a Row for the [ProxyDatabase] +/// which is just a [BTreeMap]<[String], [Value]> +#[derive(Clone, Debug, Default)] +pub struct ProxyRow { + /// The values of the single row + pub values: BTreeMap, +} + +impl ProxyRow { + /// Create a new [ProxyRow] from a [BTreeMap]<[String], [Value]> + pub fn new(values: BTreeMap) -> Self { + Self { values } + } +} + +impl From> for ProxyRow { + fn from(values: BTreeMap) -> Self { + Self { values } + } +} + +impl From for BTreeMap { + fn from(row: ProxyRow) -> Self { + row.values + } +} + +impl From for Vec<(String, Value)> { + fn from(row: ProxyRow) -> Self { + row.values.into_iter().collect() + } +} + +impl From for QueryResult { + fn from(row: ProxyRow) -> Self { + QueryResult { + row: QueryResultRow::Proxy(row), + } + } +} + +#[cfg(feature = "with-json")] +impl From for serde_json::Value { + fn from(val: ProxyRow) -> serde_json::Value { + val.values + .into_iter() + .map(|(k, v)| (k, sea_query::sea_value_to_json_value(&v))) + .collect() + } +} + +/// Convert [QueryResult] to [ProxyRow] +pub fn from_query_result_to_proxy_row(result: &QueryResult) -> ProxyRow { + match &result.row { + #[cfg(feature = "sqlx-mysql")] + QueryResultRow::SqlxMySql(row) => crate::from_sqlx_mysql_row_to_proxy_row(row), + #[cfg(feature = "sqlx-postgres")] + QueryResultRow::SqlxPostgres(row) => crate::from_sqlx_postgres_row_to_proxy_row(row), + #[cfg(feature = "sqlx-sqlite")] + QueryResultRow::SqlxSqlite(row) => crate::from_sqlx_sqlite_row_to_proxy_row(row), + #[cfg(feature = "mock")] + QueryResultRow::Mock(row) => ProxyRow { + values: row.values.clone(), + }, + QueryResultRow::Proxy(row) => row.to_owned(), + } +} + +impl ProxyRow { + /// Get a value from the [ProxyRow] + pub fn try_get(&self, index: I) -> Result + where + T: ValueType, + { + if let Some(index) = index.as_str() { + T::try_from( + self.values + .get(index) + .ok_or_else(|| query_err(format!("No column for ColIdx {index:?}")))? + .clone(), + ) + .map_err(type_err) + } else if let Some(index) = index.as_usize() { + let (_, value) = self + .values + .iter() + .nth(*index) + .ok_or_else(|| query_err(format!("Column at index {index} not found")))?; + T::try_from(value.clone()).map_err(type_err) + } else { + unreachable!("Missing ColIdx implementation for ProxyRow"); + } + } + + /// An iterator over the keys and values of a proxy row + pub fn into_column_value_tuples(self) -> impl Iterator { + self.values.into_iter() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Database, DbBackend, DbErr, ProxyDatabaseTrait, ProxyExecResult, ProxyRow, Statement, + entity::*, tests_cfg::*, + }; + use std::sync::Arc; + + #[derive(Debug)] + struct ProxyDb {} + + impl ProxyDatabaseTrait for ProxyDb { + fn query(&self, statement: Statement) -> Result, DbErr> { + println!("SQL query: {}", statement.sql); + Ok(vec![].into()) + } + + fn execute(&self, statement: Statement) -> Result { + println!("SQL execute: {}", statement.sql); + Ok(ProxyExecResult { + last_insert_id: 1, + rows_affected: 1, + }) + } + } + + #[test] + fn create_proxy_conn() { + let _db = + Database::connect_proxy(DbBackend::MySql, Arc::new(Box::new(ProxyDb {}))).unwrap(); + } + + #[test] + fn select_rows() { + let db = Database::connect_proxy(DbBackend::MySql, Arc::new(Box::new(ProxyDb {}))).unwrap(); + + let _ = cake::Entity::find().all(&db); + } + + #[test] + fn insert_one_row() { + let db = Database::connect_proxy(DbBackend::MySql, Arc::new(Box::new(ProxyDb {}))).unwrap(); + + let item = cake::ActiveModel { + id: NotSet, + name: Set("Alice".to_string()), + }; + + cake::Entity::insert(item).exec(&db).unwrap(); + } +} diff --git a/sea-orm-sync/src/database/restricted_connection.rs b/sea-orm-sync/src/database/restricted_connection.rs new file mode 100644 index 0000000000..393d485e6d --- /dev/null +++ b/sea-orm-sync/src/database/restricted_connection.rs @@ -0,0 +1,493 @@ +use crate::{ + AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, + ExecResult, IsolationLevel, QueryResult, Statement, StatementBuilder, TransactionError, + TransactionSession, TransactionTrait, +}; +use crate::{ + TransactionOptions, + rbac::{ + PermissionRequest, RbacEngine, RbacError, RbacPermissionsByResources, + RbacResourcesAndPermissions, RbacRoleHierarchyList, RbacRolesAndRanks, + RbacUserRolePermissions, ResourceRequest, + entity::{role::RoleId, user::UserId}, + }, +}; +use std::{ + pin::Pin, + sync::{Arc, RwLock}, +}; +use tracing::instrument; + +/// Wrapper of [`DatabaseConnection`] that performs authorization on all executed +/// queries for the current user. Note that raw SQL [`Statement`] is not allowed +/// currently. +#[derive(Debug, Clone)] +#[cfg_attr(docsrs, doc(cfg(feature = "rbac")))] +pub struct RestrictedConnection { + pub(crate) user_id: UserId, + pub(crate) conn: DatabaseConnection, +} + +/// Wrapper of [`DatabaseTransaction`] that performs authorization on all executed +/// queries for the current user. Note that raw SQL [`Statement`] is not allowed +/// currently. +#[derive(Debug)] +pub struct RestrictedTransaction { + user_id: UserId, + conn: DatabaseTransaction, + rbac: RbacEngineMount, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct RbacEngineMount { + inner: Arc>>, +} + +impl ConnectionTrait for RestrictedConnection { + fn get_database_backend(&self) -> DbBackend { + self.conn.get_database_backend() + } + + fn execute_raw(&self, stmt: Statement) -> Result { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {stmt}" + ))) + } + + fn execute(&self, stmt: &S) -> Result { + self.user_can_run(stmt)?; + self.conn.execute(stmt) + } + + fn execute_unprepared(&self, sql: &str) -> Result { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {sql}" + ))) + } + + fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {stmt}" + ))) + } + + fn query_one(&self, stmt: &S) -> Result, DbErr> { + self.user_can_run(stmt)?; + self.conn.query_one(stmt) + } + + fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {stmt}" + ))) + } + + fn query_all(&self, stmt: &S) -> Result, DbErr> { + self.user_can_run(stmt)?; + self.conn.query_all(stmt) + } +} + +impl ConnectionTrait for RestrictedTransaction { + fn get_database_backend(&self) -> DbBackend { + self.conn.get_database_backend() + } + + fn execute_raw(&self, stmt: Statement) -> Result { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {stmt}" + ))) + } + + fn execute(&self, stmt: &S) -> Result { + self.user_can_run(stmt)?; + self.conn.execute(stmt) + } + + fn execute_unprepared(&self, sql: &str) -> Result { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {sql}" + ))) + } + + fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {stmt}" + ))) + } + + fn query_one(&self, stmt: &S) -> Result, DbErr> { + self.user_can_run(stmt)?; + self.conn.query_one(stmt) + } + + fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { + Err(DbErr::RbacError(format!( + "Raw query is not supported: {stmt}" + ))) + } + + fn query_all(&self, stmt: &S) -> Result, DbErr> { + self.user_can_run(stmt)?; + self.conn.query_all(stmt) + } +} + +impl RestrictedConnection { + /// Get the [`RbacUserId`] bounded to this connection. + pub fn user_id(&self) -> UserId { + self.user_id + } + + /// Returns `()` if the current user can execute / query the given SQL statement. + /// Returns `DbErr::AccessDenied` otherwise. + pub fn user_can_run(&self, stmt: &S) -> Result<(), DbErr> { + self.conn.rbac.user_can_run(self.user_id, stmt) + } + + /// Returns true if the current user can perform action on resource + pub fn user_can(&self, permission: P, resource: R) -> Result + where + P: Into, + R: Into, + { + self.conn.rbac.user_can(self.user_id, permission, resource) + } + + /// Get current user's role and associated permissions. + /// This includes permissions "inherited" from child roles. + pub fn current_user_role_permissions(&self) -> Result { + self.conn.rbac.user_role_permissions(self.user_id) + } + + /// Get a list of all roles and their ranks. + /// Rank is defined as (1 + number of child roles). + pub fn roles_and_ranks(&self) -> Result { + self.conn.rbac.roles_and_ranks() + } + + /// Get two lists of all resources and permissions, excluding wildcards. + pub fn resources_and_permissions(&self) -> Result { + self.conn.rbac.resources_and_permissions() + } + + /// Get a list of edges walking the role hierarchy tree + pub fn role_hierarchy_edges(&self, role_id: RoleId) -> Result { + self.conn.rbac.role_hierarchy_edges(role_id) + } + + /// Get a list of permissions for the specific role, grouped by resources. + /// This does not include permissions of child roles. + pub fn role_permissions_by_resources( + &self, + role_id: RoleId, + ) -> Result { + self.conn.rbac.role_permissions_by_resources(role_id) + } +} + +impl RestrictedTransaction { + /// Get the [`RbacUserId`] bounded to this connection. + pub fn user_id(&self) -> UserId { + self.user_id + } + + /// Returns `()` if the current user can execute / query the given SQL statement. + /// Returns `DbErr::AccessDenied` otherwise. + pub fn user_can_run(&self, stmt: &S) -> Result<(), DbErr> { + self.rbac.user_can_run(self.user_id, stmt) + } + + /// Returns true if the current user can perform action on resource + pub fn user_can(&self, permission: P, resource: R) -> Result + where + P: Into, + R: Into, + { + self.rbac.user_can(self.user_id, permission, resource) + } +} + +impl TransactionTrait for RestrictedConnection { + type Transaction = RestrictedTransaction; + + #[instrument(level = "trace")] + fn begin(&self) -> Result { + Ok(RestrictedTransaction { + user_id: self.user_id, + conn: self.conn.begin()?, + rbac: self.conn.rbac.clone(), + }) + } + + #[instrument(level = "trace")] + fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + Ok(RestrictedTransaction { + user_id: self.user_id, + conn: self.conn.begin_with_config(isolation_level, access_mode)?, + rbac: self.conn.rbac.clone(), + }) + } + + #[instrument(level = "trace")] + fn begin_with_options( + &self, + options: TransactionOptions, + ) -> Result { + Ok(RestrictedTransaction { + user_id: self.user_id, + conn: self.conn.begin_with_options(options)?, + rbac: self.conn.rbac.clone(), + }) + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + #[instrument(level = "trace", skip(callback))] + fn transaction(&self, callback: F) -> Result> + where + F: for<'c> FnOnce(&'c RestrictedTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let transaction = self.begin().map_err(TransactionError::Connection)?; + transaction.run(callback) + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + #[instrument(level = "trace", skip(callback))] + fn transaction_with_config( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'c> FnOnce(&'c RestrictedTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let transaction = self + .begin_with_config(isolation_level, access_mode) + .map_err(TransactionError::Connection)?; + transaction.run(callback) + } +} + +impl TransactionTrait for RestrictedTransaction { + type Transaction = RestrictedTransaction; + + #[instrument(level = "trace")] + fn begin(&self) -> Result { + Ok(RestrictedTransaction { + user_id: self.user_id, + conn: self.conn.begin()?, + rbac: self.rbac.clone(), + }) + } + + #[instrument(level = "trace")] + fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + Ok(RestrictedTransaction { + user_id: self.user_id, + conn: self.conn.begin_with_config(isolation_level, access_mode)?, + rbac: self.rbac.clone(), + }) + } + + #[instrument(level = "trace")] + fn begin_with_options( + &self, + options: TransactionOptions, + ) -> Result { + Ok(RestrictedTransaction { + user_id: self.user_id, + conn: self.conn.begin_with_options(options)?, + rbac: self.rbac.clone(), + }) + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + #[instrument(level = "trace", skip(callback))] + fn transaction(&self, callback: F) -> Result> + where + F: for<'c> FnOnce(&'c RestrictedTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let transaction = self.begin().map_err(TransactionError::Connection)?; + transaction.run(callback) + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. If it does not return an error, the transaction will be committed. + #[instrument(level = "trace", skip(callback))] + fn transaction_with_config( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'c> FnOnce(&'c RestrictedTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let transaction = self + .begin_with_config(isolation_level, access_mode) + .map_err(TransactionError::Connection)?; + transaction.run(callback) + } +} + +impl TransactionSession for RestrictedTransaction { + fn commit(self) -> Result<(), DbErr> { + self.commit() + } + + fn rollback(self) -> Result<(), DbErr> { + self.rollback() + } +} + +impl RestrictedTransaction { + /// Runs a transaction to completion passing through the result. + /// Rolling back the transaction on encountering an error. + #[instrument(level = "trace", skip(callback))] + fn run(self, callback: F) -> Result> + where + F: for<'b> FnOnce(&'b RestrictedTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let res = callback(&self).map_err(TransactionError::Transaction); + if res.is_ok() { + self.commit().map_err(TransactionError::Connection)?; + } else { + self.rollback().map_err(TransactionError::Connection)?; + } + res + } + + /// Commit a transaction + #[instrument(level = "trace")] + pub fn commit(self) -> Result<(), DbErr> { + self.conn.commit() + } + + /// Rolls back a transaction explicitly + #[instrument(level = "trace")] + pub fn rollback(self) -> Result<(), DbErr> { + self.conn.rollback() + } +} + +impl RbacEngineMount { + pub fn is_some(&self) -> bool { + let engine = self.inner.read().expect("RBAC Engine died"); + engine.is_some() + } + + pub fn replace(&self, engine: RbacEngine) { + let mut inner = self.inner.write().expect("RBAC Engine died"); + *inner = Some(engine); + } + + pub fn user_can(&self, user_id: UserId, permission: P, resource: R) -> Result + where + P: Into, + R: Into, + { + let permission = permission.into(); + let resource = resource.into(); + // There is nothing we can do if RwLock is poisoned. + let holder = self.inner.read().expect("RBAC Engine died"); + // Constructor of this struct should ensure engine is not None. + let engine = holder.as_ref().expect("RBAC Engine not set"); + engine + .user_can(user_id, permission, resource) + .map_err(map_err) + } + + pub fn user_can_run( + &self, + user_id: UserId, + stmt: &S, + ) -> Result<(), DbErr> { + let audit = match stmt.audit() { + Ok(audit) => audit, + Err(err) => return Err(DbErr::RbacError(err.to_string())), + }; + // There is nothing we can do if RwLock is poisoned. + let holder = self.inner.read().expect("RBAC Engine died"); + // Constructor of this struct should ensure engine is not None. + let engine = holder.as_ref().expect("RBAC Engine not set"); + for request in audit.requests { + let permission = || PermissionRequest { + action: request.access_type.as_str().to_owned(), + }; + let resource = || ResourceRequest { + schema: request.schema_table.0.as_ref().map(|s| s.1.to_string()), + table: request.schema_table.1.to_string(), + }; + if !engine + .user_can(user_id, permission(), resource()) + .map_err(map_err)? + { + return Err(DbErr::AccessDenied { + permission: permission().action.to_owned(), + resource: resource().to_string(), + }); + } + } + Ok(()) + } + + pub fn user_role_permissions(&self, user_id: UserId) -> Result { + let holder = self.inner.read().expect("RBAC Engine died"); + let engine = holder.as_ref().expect("RBAC Engine not set"); + engine + .get_user_role_permissions(user_id) + .map_err(|err| DbErr::RbacError(err.to_string())) + } + + pub fn roles_and_ranks(&self) -> Result { + let holder = self.inner.read().expect("RBAC Engine died"); + let engine = holder.as_ref().expect("RBAC Engine not set"); + engine + .get_roles_and_ranks() + .map_err(|err| DbErr::RbacError(err.to_string())) + } + + pub fn resources_and_permissions(&self) -> Result { + let holder = self.inner.read().expect("RBAC Engine died"); + let engine = holder.as_ref().expect("RBAC Engine not set"); + Ok(engine.list_resources_and_permissions()) + } + + pub fn role_hierarchy_edges(&self, role_id: RoleId) -> Result { + let holder = self.inner.read().expect("RBAC Engine died"); + let engine = holder.as_ref().expect("RBAC Engine not set"); + Ok(engine.list_role_hierarchy_edges(role_id)) + } + + pub fn role_permissions_by_resources( + &self, + role_id: RoleId, + ) -> Result { + let holder = self.inner.read().expect("RBAC Engine died"); + let engine = holder.as_ref().expect("RBAC Engine not set"); + engine + .list_role_permissions_by_resources(role_id) + .map_err(|err| DbErr::RbacError(err.to_string())) + } +} + +fn map_err(err: RbacError) -> DbErr { + DbErr::RbacError(err.to_string()) +} diff --git a/sea-orm-sync/src/database/sea_schema_rusqlite.rs b/sea-orm-sync/src/database/sea_schema_rusqlite.rs new file mode 100644 index 0000000000..3e8912fe49 --- /dev/null +++ b/sea-orm-sync/src/database/sea_schema_rusqlite.rs @@ -0,0 +1,61 @@ +use crate::{ + ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, QueryResult, QueryResultRow, + RuntimeErr, Statement, driver::rusqlite::RusqliteRow as OurRusqliteRow, +}; +use sea_query::SelectStatement; +use sea_schema::rusqlite_types::{RusqliteError, RusqliteRow}; +use std::sync::Arc; + +impl sea_schema::Connection for DatabaseConnection { + fn query_all(&self, select: SelectStatement) -> Result, RusqliteError> { + map_result(ConnectionTrait::query_all(self, &select)) + } + + fn query_all_raw(&self, sql: String) -> Result, RusqliteError> { + map_result(ConnectionTrait::query_all_raw( + self, + Statement::from_string(self.get_database_backend(), sql), + )) + } +} + +impl sea_schema::Connection for DatabaseTransaction { + fn query_all(&self, select: SelectStatement) -> Result, RusqliteError> { + map_result(ConnectionTrait::query_all(self, &select)) + } + + fn query_all_raw(&self, sql: String) -> Result, RusqliteError> { + map_result(ConnectionTrait::query_all_raw( + self, + Statement::from_string(self.get_database_backend(), sql), + )) + } +} + +fn map_result(result: Result, DbErr>) -> Result, RusqliteError> { + match result { + Ok(rows) => Ok(rows + .into_iter() + .filter_map(|r| match r.row { + #[cfg(feature = "rusqlite")] + QueryResultRow::Rusqlite(OurRusqliteRow { values, .. }) => { + Some(RusqliteRow { values }) + } + #[allow(unreachable_patterns)] + _ => None, + }) + .collect()), + Err(err) => Err(match err { + DbErr::Conn(RuntimeErr::Rusqlite(err)) => { + Arc::into_inner(err).expect("Should only have one owner") + } + DbErr::Exec(RuntimeErr::Rusqlite(err)) => { + Arc::into_inner(err).expect("Should only have one owner") + } + DbErr::Query(RuntimeErr::Rusqlite(err)) => { + Arc::into_inner(err).expect("Should only have one owner") + } + _ => RusqliteError::InvalidParameterName(err.to_string()), + }), + } +} diff --git a/sea-orm-sync/src/database/sea_schema_shim.rs b/sea-orm-sync/src/database/sea_schema_shim.rs new file mode 100644 index 0000000000..c1b5828957 --- /dev/null +++ b/sea-orm-sync/src/database/sea_schema_shim.rs @@ -0,0 +1,94 @@ +use crate::{ + ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, QueryResult, QueryResultRow, + RuntimeErr, Statement, +}; +use sea_query::SelectStatement; +use sea_schema::sqlx_types::SqlxRow; +use sqlx::Error as SqlxError; +use std::sync::Arc; + +impl sea_schema::Connection for DatabaseConnection { + fn query_all(&self, select: SelectStatement) -> Result, SqlxError> { + map_result(ConnectionTrait::query_all(self, &select)) + } + + fn query_all_raw(&self, sql: String) -> Result, SqlxError> { + map_result(ConnectionTrait::query_all_raw( + self, + Statement::from_string(self.get_database_backend(), sql), + )) + } +} + +impl sea_schema::Connection for DatabaseTransaction { + fn query_all(&self, select: SelectStatement) -> Result, SqlxError> { + map_result(ConnectionTrait::query_all(self, &select)) + } + + fn query_all_raw(&self, sql: String) -> Result, SqlxError> { + map_result(ConnectionTrait::query_all_raw( + self, + Statement::from_string(self.get_database_backend(), sql), + )) + } +} + +impl sea_schema::Connection for crate::DatabaseExecutor<'_> { + fn query_all(&self, select: SelectStatement) -> Result, SqlxError> { + match self { + crate::DatabaseExecutor::Connection(conn) => { + ::query_all(conn, select) + } + crate::DatabaseExecutor::Transaction(txn) => { + ::query_all(txn, select) + } + crate::DatabaseExecutor::OwnedTransaction(txn) => { + ::query_all(txn, select) + } + } + } + + fn query_all_raw(&self, sql: String) -> Result, SqlxError> { + match self { + crate::DatabaseExecutor::Connection(conn) => { + ::query_all_raw(conn, sql) + } + crate::DatabaseExecutor::Transaction(txn) => { + ::query_all_raw(txn, sql) + } + crate::DatabaseExecutor::OwnedTransaction(txn) => { + ::query_all_raw(txn, sql) + } + } + } +} + +fn map_result(result: Result, DbErr>) -> Result, SqlxError> { + match result { + Ok(rows) => Ok(rows + .into_iter() + .filter_map(|r| match r.row { + #[cfg(feature = "sqlx-mysql")] + QueryResultRow::SqlxMySql(r) => Some(SqlxRow::MySql(r)), + #[cfg(feature = "sqlx-postgres")] + QueryResultRow::SqlxPostgres(r) => Some(SqlxRow::Postgres(r)), + #[cfg(feature = "sqlx-sqlite")] + QueryResultRow::SqlxSqlite(r) => Some(SqlxRow::Sqlite(r)), + #[allow(unreachable_patterns)] + _ => None, + }) + .collect()), + Err(err) => Err(match err { + DbErr::Conn(RuntimeErr::SqlxError(err)) => { + Arc::into_inner(err).expect("Should only have one owner") + } + DbErr::Exec(RuntimeErr::SqlxError(err)) => { + Arc::into_inner(err).expect("Should only have one owner") + } + DbErr::Query(RuntimeErr::SqlxError(err)) => { + Arc::into_inner(err).expect("Should only have one owner") + } + _ => SqlxError::AnyDriverError(Box::new(err)), + }), + } +} diff --git a/sea-orm-sync/src/database/statement.rs b/sea-orm-sync/src/database/statement.rs new file mode 100644 index 0000000000..2c35e04298 --- /dev/null +++ b/sea-orm-sync/src/database/statement.rs @@ -0,0 +1,172 @@ +use crate::DbBackend; +#[cfg(feature = "rbac")] +pub use sea_query::audit::{AuditTrait, Error as AuditError, QueryAccessAudit}; +use sea_query::{MysqlQueryBuilder, PostgresQueryBuilder, SqliteQueryBuilder, inject_parameters}; +pub use sea_query::{Value, Values}; +use std::fmt; + +/// Defines an SQL statement +#[derive(Debug, Clone, PartialEq)] +pub struct Statement { + /// The SQL query + pub sql: String, + /// The values for the SQL statement's parameters + pub values: Option, + /// The database backend this statement is constructed for. + /// The SQL dialect and values should be valid for the DbBackend. + pub db_backend: DbBackend, +} + +/// Any type that can build a [Statement] +pub trait StatementBuilder { + /// Method to build a [Statement] + fn build(&self, db_backend: &DbBackend) -> Statement; + + #[cfg(feature = "rbac")] + /// Method to audit access request of query + fn audit(&self) -> Result; +} + +impl Statement { + /// Create a [Statement] from a [crate::DatabaseBackend] and a raw SQL statement + pub fn from_string(db_backend: DbBackend, stmt: T) -> Statement + where + T: Into, + { + Statement { + sql: stmt.into(), + values: None, + db_backend, + } + } + + /// Create a SQL statement from a [crate::DatabaseBackend], a + /// raw SQL statement and param values + pub fn from_sql_and_values(db_backend: DbBackend, sql: T, values: I) -> Self + where + I: IntoIterator, + T: Into, + { + Self::from_string_values_tuple(db_backend, (sql, Values(values.into_iter().collect()))) + } + + pub(crate) fn from_string_values_tuple(db_backend: DbBackend, stmt: (T, Values)) -> Statement + where + T: Into, + { + Statement { + sql: stmt.0.into(), + values: Some(stmt.1), + db_backend, + } + } +} + +impl fmt::Display for Statement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.values { + Some(values) => { + let string = match self.db_backend { + DbBackend::MySql => inject_parameters(&self.sql, &values.0, &MysqlQueryBuilder), + DbBackend::Postgres => { + inject_parameters(&self.sql, &values.0, &PostgresQueryBuilder) + } + DbBackend::Sqlite => { + inject_parameters(&self.sql, &values.0, &SqliteQueryBuilder) + } + }; + write!(f, "{}", &string) + } + None => { + write!(f, "{}", &self.sql) + } + } + } +} + +macro_rules! build_any_stmt { + ($stmt: expr, $db_backend: expr) => { + match $db_backend { + DbBackend::MySql => $stmt.build(MysqlQueryBuilder), + DbBackend::Postgres => $stmt.build(PostgresQueryBuilder), + DbBackend::Sqlite => $stmt.build(SqliteQueryBuilder), + } + }; +} + +macro_rules! build_postgres_stmt { + ($stmt: expr, $db_backend: expr) => { + match $db_backend { + DbBackend::Postgres => $stmt.to_string(PostgresQueryBuilder), + DbBackend::MySql | DbBackend::Sqlite => unimplemented!(), + } + }; +} + +macro_rules! build_query_stmt { + ($stmt: ty) => { + impl StatementBuilder for $stmt { + fn build(&self, db_backend: &DbBackend) -> Statement { + let stmt = build_any_stmt!(self, db_backend); + Statement::from_string_values_tuple(*db_backend, stmt) + } + + #[cfg(feature = "rbac")] + fn audit(&self) -> Result { + AuditTrait::audit(self) + } + } + }; +} + +build_query_stmt!(sea_query::InsertStatement); +build_query_stmt!(sea_query::SelectStatement); +build_query_stmt!(sea_query::UpdateStatement); +build_query_stmt!(sea_query::DeleteStatement); +build_query_stmt!(sea_query::WithQuery); + +macro_rules! build_schema_stmt { + ($stmt: ty) => { + impl StatementBuilder for $stmt { + fn build(&self, db_backend: &DbBackend) -> Statement { + let stmt = build_any_stmt!(self, db_backend); + Statement::from_string(*db_backend, stmt) + } + + #[cfg(feature = "rbac")] + fn audit(&self) -> Result { + todo!("Audit not supported for {} yet", stringify!($stmt)) + } + } + }; +} + +build_schema_stmt!(sea_query::TableCreateStatement); +build_schema_stmt!(sea_query::TableDropStatement); +build_schema_stmt!(sea_query::TableAlterStatement); +build_schema_stmt!(sea_query::TableRenameStatement); +build_schema_stmt!(sea_query::TableTruncateStatement); +build_schema_stmt!(sea_query::IndexCreateStatement); +build_schema_stmt!(sea_query::IndexDropStatement); +build_schema_stmt!(sea_query::ForeignKeyCreateStatement); +build_schema_stmt!(sea_query::ForeignKeyDropStatement); + +macro_rules! build_type_stmt { + ($stmt: ty) => { + impl StatementBuilder for $stmt { + fn build(&self, db_backend: &DbBackend) -> Statement { + let stmt = build_postgres_stmt!(self, db_backend); + Statement::from_string(*db_backend, stmt) + } + + #[cfg(feature = "rbac")] + fn audit(&self) -> Result { + Err(AuditError::UnsupportedQuery) + } + } + }; +} + +build_type_stmt!(sea_query::extension::postgres::TypeAlterStatement); +build_type_stmt!(sea_query::extension::postgres::TypeCreateStatement); +build_type_stmt!(sea_query::extension::postgres::TypeDropStatement); diff --git a/sea-orm-sync/src/database/stream/metric.rs b/sea-orm-sync/src/database/stream/metric.rs new file mode 100644 index 0000000000..e1daacc092 --- /dev/null +++ b/sea-orm-sync/src/database/stream/metric.rs @@ -0,0 +1,86 @@ +use std::time::Duration; + +use crate::{DbErr, QueryResult, Statement}; + +#[cfg(not(feature = "sync"))] +type PinBoxStream<'a> = Pin> + 'a>>; +#[cfg(feature = "sync")] +type PinBoxStream<'a> = Box> + 'a>; + +pub(crate) struct MetricStream<'a> { + metric_callback: &'a Option, + stmt: &'a Statement, + elapsed: Option, + stream: PinBoxStream<'a>, +} + +impl<'a> MetricStream<'a> { + #[allow(dead_code)] + pub(crate) fn new( + metric_callback: &'a Option, + stmt: &'a Statement, + elapsed: Option, + stream: S, + ) -> Self + where + S: Iterator> + 'a, + { + MetricStream { + metric_callback, + stmt, + elapsed, + stream: Box::new(stream), + } + } +} + +#[cfg(not(feature = "sync"))] +impl Stream for MetricStream<'_> { + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + let _start = this + .metric_callback + .is_some() + .then(std::time::SystemTime::now); + let res = Pin::new(&mut this.stream).poll_next(cx); + if let (Some(_start), Some(elapsed)) = (_start, &mut this.elapsed) { + *elapsed += _start.elapsed().unwrap_or_default(); + } + res + } +} + +#[cfg(feature = "sync")] +impl Iterator for MetricStream<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + let _start = self + .metric_callback + .is_some() + .then(std::time::SystemTime::now); + let res = self.stream.next(); + if let (Some(_start), Some(elapsed)) = (_start, &mut self.elapsed) { + *elapsed += _start.elapsed().unwrap_or_default(); + } + res + } +} + +impl Drop for MetricStream<'_> { + fn drop(&mut self) { + if let (Some(callback), Some(elapsed)) = (self.metric_callback.as_deref(), self.elapsed) { + let info = crate::metric::Info { + elapsed, + statement: self.stmt, + failed: false, + }; + callback(&info); + } + } +} diff --git a/sea-orm-sync/src/database/stream/mod.rs b/sea-orm-sync/src/database/stream/mod.rs new file mode 100644 index 0000000000..deb4495f05 --- /dev/null +++ b/sea-orm-sync/src/database/stream/mod.rs @@ -0,0 +1,7 @@ +mod metric; + +mod query; +mod transaction; + +pub use query::*; +pub use transaction::*; diff --git a/sea-orm-sync/src/database/stream/query.rs b/sea-orm-sync/src/database/stream/query.rs new file mode 100644 index 0000000000..817c3f44ab --- /dev/null +++ b/sea-orm-sync/src/database/stream/query.rs @@ -0,0 +1,137 @@ +#![allow(missing_docs, unreachable_code, unused_variables)] + +use tracing::instrument; + +#[cfg(feature = "sqlx-dep")] +use futures_util::TryStreamExt; + +#[cfg(feature = "sqlx-dep")] +use sqlx::Executor; + +use super::metric::MetricStream; +#[cfg(feature = "sqlx-dep")] +use crate::driver::*; +use crate::{DbErr, InnerConnection, QueryResult, Statement}; + +/// Creates a stream from a [QueryResult] +#[ouroboros::self_referencing] +pub struct QueryStream { + stmt: Statement, + conn: InnerConnection, + metric_callback: Option, + #[borrows(mut conn, stmt, metric_callback)] + #[not_covariant] + stream: MetricStream<'this>, +} + +impl std::fmt::Debug for QueryStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "QueryStream") + } +} + +impl QueryStream { + #[allow(dead_code)] + #[instrument(level = "trace", skip(metric_callback))] + pub(crate) fn build( + stmt: Statement, + conn: InnerConnection, + metric_callback: Option, + ) -> QueryStream { + QueryStreamBuilder { + stmt, + conn, + metric_callback, + stream_builder: |conn, stmt, _metric_callback| match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(c) => { + let query = crate::driver::sqlx_mysql::sqlx_query(stmt); + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c + .fetch(query) + .map_ok(Into::into) + .map_err(sqlx_error_to_query_err); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(c) => { + let query = crate::driver::sqlx_postgres::sqlx_query(stmt); + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c + .fetch(query) + .map_ok(Into::into) + .map_err(sqlx_error_to_query_err); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(c) => { + let query = crate::driver::sqlx_sqlite::sqlx_query(stmt); + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c + .fetch(query) + .map_ok(Into::into) + .map_err(sqlx_error_to_query_err); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(conn) => { + use itertools::Either; + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = match conn.stream(stmt) { + Ok(rows) => Either::Left(rows.into_iter().map(Ok)), + Err(err) => Either::Right(std::iter::once(Err(err))), + }; + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "mock")] + InnerConnection::Mock(c) => { + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c.fetch(stmt); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(c) => { + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = futures_util::stream::once({ + Err(DbErr::BackendNotSupported { + db: "Proxy", + ctx: "QueryStream", + }) + }); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[allow(unreachable_patterns)] + _ => unreachable!(), + }, + } + .build() + } +} + +#[cfg(not(feature = "sync"))] +impl Stream for QueryStream { + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + this.with_stream_mut(|stream| Pin::new(stream).poll_next(cx)) + } +} + +#[cfg(feature = "sync")] +impl Iterator for QueryStream { + type Item = Result; + + fn next(&mut self) -> Option { + self.with_stream_mut(|stream| stream.next()) + } +} diff --git a/sea-orm-sync/src/database/stream/transaction.rs b/sea-orm-sync/src/database/stream/transaction.rs new file mode 100644 index 0000000000..41b4f863bc --- /dev/null +++ b/sea-orm-sync/src/database/stream/transaction.rs @@ -0,0 +1,130 @@ +#![allow(missing_docs)] + +use std::ops::DerefMut; +use tracing::instrument; + +#[cfg(feature = "sqlx-dep")] +use futures_util::TryStreamExt; + +use std::sync::MutexGuard; + +#[cfg(feature = "sqlx-dep")] +use sqlx::Executor; + +use super::metric::MetricStream; +#[cfg(feature = "sqlx-dep")] +use crate::driver::*; +use crate::{DbErr, InnerConnection, QueryResult, Statement}; + +/// `TransactionStream` cannot be used in a `transaction` closure as it does not impl `Send`. +/// It seems to be a Rust limitation right now, and solution to work around this deemed to be extremely hard. +#[ouroboros::self_referencing] +pub struct TransactionStream<'a> { + stmt: Statement, + conn: MutexGuard<'a, InnerConnection>, + metric_callback: Option, + #[borrows(mut conn, stmt, metric_callback)] + #[not_covariant] + stream: MetricStream<'this>, +} + +impl std::fmt::Debug for TransactionStream<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TransactionStream") + } +} + +impl TransactionStream<'_> { + #[instrument(level = "trace", skip(metric_callback))] + #[allow(unused_variables)] + pub(crate) fn build( + conn: MutexGuard<'_, InnerConnection>, + stmt: Statement, + metric_callback: Option, + ) -> TransactionStream<'_> { + TransactionStreamBuilder { + stmt, + conn, + metric_callback, + stream_builder: |conn, stmt, _metric_callback| match conn.deref_mut() { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(c) => { + let query = crate::driver::sqlx_mysql::sqlx_query(stmt); + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c + .fetch(query) + .map_ok(Into::into) + .map_err(sqlx_error_to_query_err); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(c) => { + let query = crate::driver::sqlx_postgres::sqlx_query(stmt); + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c + .fetch(query) + .map_ok(Into::into) + .map_err(sqlx_error_to_query_err); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(c) => { + let query = crate::driver::sqlx_sqlite::sqlx_query(stmt); + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c + .fetch(query) + .map_ok(Into::into) + .map_err(sqlx_error_to_query_err); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "mock")] + InnerConnection::Mock(c) => { + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = c.fetch(stmt); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(c) => { + let start = _metric_callback.is_some().then(std::time::SystemTime::now); + let stream = futures_util::stream::once({ + Err(DbErr::BackendNotSupported { + db: "Proxy", + ctx: "TransactionStream", + }) + }); + let elapsed = start.map(|s| s.elapsed().unwrap_or_default()); + MetricStream::new(_metric_callback, stmt, elapsed, stream) + } + #[allow(unreachable_patterns)] + _ => unreachable!(), + }, + } + .build() + } +} + +#[cfg(not(feature = "sync"))] +impl Stream for TransactionStream<'_> { + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + this.with_stream_mut(|stream| Pin::new(stream).poll_next(cx)) + } +} + +#[cfg(feature = "sync")] +impl Iterator for TransactionStream<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + self.with_stream_mut(|stream| stream.next()) + } +} diff --git a/sea-orm-sync/src/database/tracing_spans.rs b/sea-orm-sync/src/database/tracing_spans.rs new file mode 100644 index 0000000000..ce5e709c8b --- /dev/null +++ b/sea-orm-sync/src/database/tracing_spans.rs @@ -0,0 +1,231 @@ +//! Tracing support for database operations. +//! +//! This module provides utilities for instrumenting database operations +//! with tracing spans. Enable the `tracing-spans` feature to automatically +//! generate spans for all database operations. +//! +//! # Example +//! +//! ```toml +//! [dependencies] +//! sea-orm = { version = "2.0", features = ["tracing-spans"] } +//! ``` +//! +//! ```ignore +//! use sea_orm::Database; +//! +//! // Set up a tracing subscriber +//! tracing_subscriber::fmt() +//! .with_max_level(tracing::Level::INFO) +//! .init(); +//! +//! // All database operations will now generate tracing spans +//! let db = Database::connect("sqlite::memory:")?; +//! let cakes = Cake::find().all(&db)?; // Generates a span +//! ``` + +#[cfg(feature = "tracing-spans")] +mod inner { + use crate::DbBackend; + + /// Database operation type, following OpenTelemetry conventions. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) enum DbOperation { + /// SELECT query + Select, + /// INSERT statement + Insert, + /// UPDATE statement + Update, + /// DELETE statement + Delete, + /// Other/unknown SQL execution + Execute, + } + + impl DbOperation { + /// Parse the operation type from an SQL query string. + /// + /// This function is allocation-free and uses case-insensitive comparison. + pub fn from_sql(sql: &str) -> Self { + let first_word = sql.trim_start().split_whitespace().next().unwrap_or(""); + if first_word.eq_ignore_ascii_case("SELECT") { + DbOperation::Select + } else if first_word.eq_ignore_ascii_case("INSERT") { + DbOperation::Insert + } else if first_word.eq_ignore_ascii_case("UPDATE") { + DbOperation::Update + } else if first_word.eq_ignore_ascii_case("DELETE") { + DbOperation::Delete + } else { + DbOperation::Execute + } + } + } + + impl std::fmt::Display for DbOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DbOperation::Select => write!(f, "SELECT"), + DbOperation::Insert => write!(f, "INSERT"), + DbOperation::Update => write!(f, "UPDATE"), + DbOperation::Delete => write!(f, "DELETE"), + DbOperation::Execute => write!(f, "EXECUTE"), + } + } + } + + /// Get the OpenTelemetry system name from DbBackend. + pub(crate) fn db_system_name(backend: DbBackend) -> &'static str { + match backend { + DbBackend::Postgres => "postgresql", + DbBackend::MySql => "mysql", + DbBackend::Sqlite => "sqlite", + } + } + + /// Record query result on a span (success/failure status and error message). + pub(crate) fn record_query_result( + span: &tracing::Span, + result: &Result, + ) { + match result { + Ok(_) => { + span.record("otel.status_code", "OK"); + } + Err(e) => { + span.record("otel.status_code", "ERROR"); + span.record("exception.message", tracing::field::display(e)); + } + } + } +} + +#[cfg(feature = "tracing-spans")] +pub(crate) use inner::*; + +/// Create a tracing span for database operations. +/// +/// Arguments: +/// - `$name`: Span name (e.g., "sea_orm.execute", "sea_orm.query_one") +/// - `$backend`: DbBackend value +/// - `$sql`: SQL statement string (used for operation parsing) +/// +/// Note: `db.statement` is set to Empty. Call `span.record("db.statement", sql)` +/// separately if the query is parameterized (safe to log). +#[cfg(feature = "tracing-spans")] +macro_rules! db_span { + ($name:expr, $backend:expr, $sql:expr) => {{ + let sql: &str = $sql; + let op = $crate::database::tracing_spans::DbOperation::from_sql(sql); + ::tracing::info_span!( + $name, + otel.kind = "client", + db.system = $crate::database::tracing_spans::db_system_name($backend), + db.operation = %op, + db.statement = ::tracing::field::Empty, + otel.status_code = ::tracing::field::Empty, + exception.message = ::tracing::field::Empty, + ) + }}; +} + +#[cfg(feature = "tracing-spans")] +pub(crate) use db_span; + +/// Execute a future and wrap it in a tracing span when `tracing-spans` is enabled. +/// +/// When the feature is disabled, this macro simply awaits the future with zero overhead. +/// +/// # Arguments +/// - `$name`: span name (e.g., "sea_orm.execute") +/// - `$backend`: DbBackend +/// - `$sql`: &str used for db.operation parsing +/// - `record_stmt`: whether to record `db.statement` +/// - `$fut`: the future to execute +macro_rules! with_db_span { + ($name:expr, $backend:expr, $sql:expr, record_stmt = $record_stmt:expr, $fut:expr) => {{ + #[cfg(all(feature = "tracing-spans", not(feature = "sync")))] + { + let span = $crate::database::tracing_spans::db_span!($name, $backend, $sql); + if $record_stmt { + span.record("db.statement", $sql); + } + let result = ::tracing::Instrument::instrument($fut, span.clone()); + $crate::database::tracing_spans::record_query_result(&span, &result); + result + } + #[cfg(all(feature = "tracing-spans", feature = "sync"))] + { + let span = $crate::database::tracing_spans::db_span!($name, $backend, $sql); + if $record_stmt { + span.record("db.statement", $sql); + } + span.in_scope($fut) + } + #[cfg(not(feature = "tracing-spans"))] + { + $fut + } + }}; +} + +pub(crate) use with_db_span; + +#[cfg(feature = "tracing-spans")] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_db_operation_from_sql() { + assert_eq!( + DbOperation::from_sql("SELECT * FROM users"), + DbOperation::Select + ); + assert_eq!( + DbOperation::from_sql(" SELECT * FROM users"), + DbOperation::Select + ); + assert_eq!( + DbOperation::from_sql("select * from users"), + DbOperation::Select + ); + assert_eq!( + DbOperation::from_sql("INSERT INTO users"), + DbOperation::Insert + ); + assert_eq!( + DbOperation::from_sql("UPDATE users SET"), + DbOperation::Update + ); + assert_eq!( + DbOperation::from_sql("DELETE FROM users"), + DbOperation::Delete + ); + assert_eq!( + DbOperation::from_sql("CREATE TABLE users"), + DbOperation::Execute + ); + assert_eq!( + DbOperation::from_sql("DROP TABLE users"), + DbOperation::Execute + ); + } + + #[test] + fn test_db_system_name() { + assert_eq!(db_system_name(DbBackend::Postgres), "postgresql"); + assert_eq!(db_system_name(DbBackend::MySql), "mysql"); + assert_eq!(db_system_name(DbBackend::Sqlite), "sqlite"); + } + + #[test] + fn test_db_operation_display() { + assert_eq!(DbOperation::Select.to_string(), "SELECT"); + assert_eq!(DbOperation::Insert.to_string(), "INSERT"); + assert_eq!(DbOperation::Update.to_string(), "UPDATE"); + assert_eq!(DbOperation::Delete.to_string(), "DELETE"); + assert_eq!(DbOperation::Execute.to_string(), "EXECUTE"); + } +} diff --git a/sea-orm-sync/src/database/transaction.rs b/sea-orm-sync/src/database/transaction.rs new file mode 100644 index 0000000000..c907335e6d --- /dev/null +++ b/sea-orm-sync/src/database/transaction.rs @@ -0,0 +1,688 @@ +#![allow(unused_assignments)] +use std::sync::Arc; + +#[cfg(feature = "sqlx-dep")] +use sqlx::TransactionManager; +use std::sync::Mutex; +use tracing::instrument; + +use crate::{ + AccessMode, ConnectionTrait, DbBackend, DbErr, ExecResult, InnerConnection, IsolationLevel, + QueryResult, SqliteTransactionMode, Statement, StreamTrait, TransactionOptions, + TransactionSession, TransactionStream, TransactionTrait, debug_print, error::*, +}; +#[cfg(feature = "sqlx-dep")] +use crate::{sqlx_error_to_exec_err, sqlx_error_to_query_err}; + +/// Defines a database transaction, whether it is an open transaction and the type of +/// backend to use. +/// Under the hood, a Transaction is just a wrapper for a connection where +/// START TRANSACTION has been executed. +pub struct DatabaseTransaction { + conn: Arc>, + backend: DbBackend, + open: bool, + metric_callback: Option, +} + +impl std::fmt::Debug for DatabaseTransaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DatabaseTransaction") + } +} + +impl DatabaseTransaction { + #[instrument(level = "trace", skip(metric_callback))] + pub(crate) fn begin( + conn: Arc>, + backend: DbBackend, + metric_callback: Option, + isolation_level: Option, + access_mode: Option, + sqlite_transaction_mode: Option, + ) -> Result { + let res = DatabaseTransaction { + conn, + backend, + open: true, + metric_callback, + }; + + let begin_result: Result<(), DbErr> = super::tracing_spans::with_db_span!( + "sea_orm.begin", + backend, + "BEGIN", + record_stmt = false, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *res.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *res.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(c) => { + // in MySQL SET TRANSACTION operations must be executed before transaction start + crate::driver::sqlx_mysql::set_transaction_config( + c, + isolation_level, + access_mode, + )?; + ::TransactionManager::begin(c, None) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(c) => { + ::TransactionManager::begin(c, None) + .map_err(sqlx_error_to_query_err)?; + // in PostgreSQL SET TRANSACTION operations must be executed inside transaction + crate::driver::sqlx_postgres::set_transaction_config( + c, + isolation_level, + access_mode, + ) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(c) => { + crate::driver::sqlx_sqlite::set_transaction_config( + c, + isolation_level, + access_mode, + )?; + let depth = ::TransactionManager::get_transaction_depth(c); + let statement = if depth == 0 { + sqlite_transaction_mode.map(|mode| { + std::borrow::Cow::from(format!("BEGIN {}", mode.sqlite_keyword())) + }) + } else { + // Nested transaction uses SAVEPOINT; the mode only applies to the top-level BEGIN + None + }; + ::TransactionManager::begin(c, statement) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(c) => c.begin(sqlite_transaction_mode), + #[cfg(feature = "mock")] + InnerConnection::Mock(c) => { + c.begin(); + Ok(()) + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(c) => { + c.begin(); + Ok(()) + } + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ); + + begin_result?; + Ok(res) + } + + /// Runs a transaction to completion passing through the result. + /// Rolling back the transaction on encountering an error. + #[instrument(level = "trace", skip(callback))] + pub(crate) fn run(self, callback: F) -> Result> + where + F: for<'b> FnOnce(&'b DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let res = callback(&self).map_err(TransactionError::Transaction); + if res.is_ok() { + self.commit().map_err(TransactionError::Connection)?; + } else { + self.rollback().map_err(TransactionError::Connection)?; + } + res + } + + /// Commit a transaction + #[instrument(level = "trace")] + #[allow(unreachable_code, unused_mut)] + pub fn commit(mut self) -> Result<(), DbErr> { + let result: Result<(), DbErr> = super::tracing_spans::with_db_span!( + "sea_orm.commit", + self.backend, + "COMMIT", + record_stmt = false, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *self.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(c) => { + ::TransactionManager::commit(c) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(c) => { + ::TransactionManager::commit(c) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(c) => { + ::TransactionManager::commit(c) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(c) => c.commit(), + #[cfg(feature = "mock")] + InnerConnection::Mock(c) => { + c.commit(); + Ok(()) + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(c) => { + c.commit(); + Ok(()) + } + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ); + + result?; + self.open = false; // read by start_rollback + Ok(()) + } + + /// Rolls back a transaction explicitly + #[instrument(level = "trace")] + #[allow(unreachable_code, unused_mut)] + pub fn rollback(mut self) -> Result<(), DbErr> { + let result: Result<(), DbErr> = super::tracing_spans::with_db_span!( + "sea_orm.rollback", + self.backend, + "ROLLBACK", + record_stmt = false, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *self.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(c) => { + ::TransactionManager::rollback(c) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(c) => { + ::TransactionManager::rollback(c) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(c) => { + ::TransactionManager::rollback(c) + .map_err(sqlx_error_to_query_err) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(c) => c.rollback(), + #[cfg(feature = "mock")] + InnerConnection::Mock(c) => { + c.rollback(); + Ok(()) + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(c) => { + c.rollback(); + Ok(()) + } + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ); + + result?; + self.open = false; // read by start_rollback + Ok(()) + } + + // the rollback is queued and will be performed on next operation, like returning the connection to the pool + #[instrument(level = "trace")] + fn start_rollback(&mut self) -> Result<(), DbErr> { + if self.open { + if let Some(mut conn) = self.conn.try_lock().ok() { + match &mut *conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(c) => { + ::TransactionManager::start_rollback(c); + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(c) => { + ::TransactionManager::start_rollback(c); + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(c) => { + ::TransactionManager::start_rollback(c); + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(c) => { + c.start_rollback()?; + } + #[cfg(feature = "mock")] + InnerConnection::Mock(c) => { + c.rollback(); + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(c) => { + c.start_rollback(); + } + #[allow(unreachable_patterns)] + _ => return Err(conn_err("Disconnected")), + } + } else { + //this should never happen + return Err(conn_err("Dropping a locked Transaction")); + } + } + Ok(()) + } +} + +impl TransactionSession for DatabaseTransaction { + fn commit(self) -> Result<(), DbErr> { + self.commit() + } + + fn rollback(self) -> Result<(), DbErr> { + self.rollback() + } +} + +impl Drop for DatabaseTransaction { + fn drop(&mut self) { + self.start_rollback().expect("Fail to rollback transaction"); + } +} + +impl ConnectionTrait for DatabaseTransaction { + fn get_database_backend(&self) -> DbBackend { + // this way we don't need to lock just to know the backend + self.backend + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn execute_raw(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + super::tracing_spans::with_db_span!( + "sea_orm.execute", + self.backend, + stmt.sql.as_str(), + record_stmt = true, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *self.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(conn) => { + let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); + let conn: &mut sqlx::MySqlConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + query.execute(conn).map(Into::into) + }) + .map_err(sqlx_error_to_exec_err) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(conn) => { + let query = crate::driver::sqlx_postgres::sqlx_query(&stmt); + let conn: &mut sqlx::PgConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + query.execute(conn).map(Into::into) + }) + .map_err(sqlx_error_to_exec_err) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(conn) => { + let query = crate::driver::sqlx_sqlite::sqlx_query(&stmt); + let conn: &mut sqlx::SqliteConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + query.execute(conn).map(Into::into) + }) + .map_err(sqlx_error_to_exec_err) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(conn) => conn.execute(stmt, &self.metric_callback), + #[cfg(feature = "mock")] + InnerConnection::Mock(conn) => conn.execute(stmt), + #[cfg(feature = "proxy")] + InnerConnection::Proxy(conn) => conn.execute(stmt), + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ) + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn execute_unprepared(&self, sql: &str) -> Result { + debug_print!("{}", sql); + + super::tracing_spans::with_db_span!( + "sea_orm.execute_unprepared", + self.backend, + sql, + record_stmt = false, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *self.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(conn) => { + let conn: &mut sqlx::MySqlConnection = &mut *conn; + sqlx::Executor::execute(conn, sql) + .map(Into::into) + .map_err(sqlx_error_to_exec_err) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(conn) => { + let conn: &mut sqlx::PgConnection = &mut *conn; + sqlx::Executor::execute(conn, sql) + .map(Into::into) + .map_err(sqlx_error_to_exec_err) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(conn) => { + let conn: &mut sqlx::SqliteConnection = &mut *conn; + sqlx::Executor::execute(conn, sql) + .map(Into::into) + .map_err(sqlx_error_to_exec_err) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(conn) => conn.execute_unprepared(sql), + #[cfg(feature = "mock")] + InnerConnection::Mock(conn) => { + let db_backend = conn.get_database_backend(); + let stmt = Statement::from_string(db_backend, sql); + conn.execute(stmt) + } + #[cfg(feature = "proxy")] + InnerConnection::Proxy(conn) => { + let db_backend = conn.get_database_backend(); + let stmt = Statement::from_string(db_backend, sql); + conn.execute(stmt) + } + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ) + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + super::tracing_spans::with_db_span!( + "sea_orm.query_one", + self.backend, + stmt.sql.as_str(), + record_stmt = true, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *self.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(conn) => { + let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); + let conn: &mut sqlx::MySqlConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + crate::sqlx_map_err_ignore_not_found( + query.fetch_one(conn).map(|row| Some(row.into())), + ) + }) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(conn) => { + let query = crate::driver::sqlx_postgres::sqlx_query(&stmt); + let conn: &mut sqlx::PgConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + crate::sqlx_map_err_ignore_not_found( + query.fetch_one(conn).map(|row| Some(row.into())), + ) + }) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(conn) => { + let query = crate::driver::sqlx_sqlite::sqlx_query(&stmt); + let conn: &mut sqlx::SqliteConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + crate::sqlx_map_err_ignore_not_found( + query.fetch_one(conn).map(|row| Some(row.into())), + ) + }) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(conn) => conn.query_one(stmt, &self.metric_callback), + #[cfg(feature = "mock")] + InnerConnection::Mock(conn) => conn.query_one(stmt), + #[cfg(feature = "proxy")] + InnerConnection::Proxy(conn) => conn.query_one(stmt), + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ) + } + + #[instrument(level = "trace")] + #[allow(unused_variables)] + fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + super::tracing_spans::with_db_span!( + "sea_orm.query_all", + self.backend, + stmt.sql.as_str(), + record_stmt = true, + { + #[cfg(not(feature = "sync"))] + let conn = &mut *self.conn.lock(); + #[cfg(feature = "sync")] + let conn = &mut *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + + match conn { + #[cfg(feature = "sqlx-mysql")] + InnerConnection::MySql(conn) => { + let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); + let conn: &mut sqlx::MySqlConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + query + .fetch_all(conn) + .map(|rows| rows.into_iter().map(|r| r.into()).collect()) + .map_err(sqlx_error_to_query_err) + }) + } + #[cfg(feature = "sqlx-postgres")] + InnerConnection::Postgres(conn) => { + let query = crate::driver::sqlx_postgres::sqlx_query(&stmt); + let conn: &mut sqlx::PgConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + query + .fetch_all(conn) + .map(|rows| rows.into_iter().map(|r| r.into()).collect()) + .map_err(sqlx_error_to_query_err) + }) + } + #[cfg(feature = "sqlx-sqlite")] + InnerConnection::Sqlite(conn) => { + let query = crate::driver::sqlx_sqlite::sqlx_query(&stmt); + let conn: &mut sqlx::SqliteConnection = &mut *conn; + crate::metric::metric!(self.metric_callback, &stmt, { + query + .fetch_all(conn) + .map(|rows| rows.into_iter().map(|r| r.into()).collect()) + .map_err(sqlx_error_to_query_err) + }) + } + #[cfg(feature = "rusqlite")] + InnerConnection::Rusqlite(conn) => conn.query_all(stmt, &self.metric_callback), + #[cfg(feature = "mock")] + InnerConnection::Mock(conn) => conn.query_all(stmt), + #[cfg(feature = "proxy")] + InnerConnection::Proxy(conn) => conn.query_all(stmt), + #[allow(unreachable_patterns)] + _ => Err(conn_err("Disconnected")), + } + } + ) + } +} + +impl StreamTrait for DatabaseTransaction { + type Stream<'a> = TransactionStream<'a>; + + fn get_database_backend(&self) -> DbBackend { + self.backend + } + + #[instrument(level = "trace")] + fn stream_raw<'a>(&'a self, stmt: Statement) -> Result, DbErr> { + ({ + #[cfg(not(feature = "sync"))] + let conn = self.conn.lock(); + #[cfg(feature = "sync")] + let conn = self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; + Ok(crate::TransactionStream::build( + conn, + stmt, + self.metric_callback.clone(), + )) + }) + } +} + +impl TransactionTrait for DatabaseTransaction { + type Transaction = DatabaseTransaction; + + #[instrument(level = "trace")] + fn begin(&self) -> Result { + DatabaseTransaction::begin( + Arc::clone(&self.conn), + self.backend, + self.metric_callback.clone(), + None, + None, + None, + ) + } + + #[instrument(level = "trace")] + fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + DatabaseTransaction::begin( + Arc::clone(&self.conn), + self.backend, + self.metric_callback.clone(), + isolation_level, + access_mode, + None, + ) + } + + #[instrument(level = "trace")] + fn begin_with_options( + &self, + options: TransactionOptions, + ) -> Result { + DatabaseTransaction::begin( + Arc::clone(&self.conn), + self.backend, + self.metric_callback.clone(), + options.isolation_level, + options.access_mode, + options.sqlite_transaction_mode, + ) + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. + /// Otherwise, the transaction will be committed. + #[instrument(level = "trace", skip(_callback))] + fn transaction(&self, _callback: F) -> Result> + where + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let transaction = self.begin().map_err(TransactionError::Connection)?; + transaction.run(_callback) + } + + /// Execute the function inside a transaction. + /// If the function returns an error, the transaction will be rolled back. + /// Otherwise, the transaction will be committed. + #[instrument(level = "trace", skip(_callback))] + fn transaction_with_config( + &self, + _callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let transaction = self + .begin_with_config(isolation_level, access_mode) + .map_err(TransactionError::Connection)?; + transaction.run(_callback) + } +} + +/// Defines errors for handling transaction failures +#[derive(Debug)] +pub enum TransactionError { + /// A Database connection error + Connection(DbErr), + /// An error occurring when doing database transactions + Transaction(E), +} + +impl std::fmt::Display for TransactionError +where + E: std::fmt::Display + std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransactionError::Connection(e) => std::fmt::Display::fmt(e, f), + TransactionError::Transaction(e) => std::fmt::Display::fmt(e, f), + } + } +} + +impl std::error::Error for TransactionError where E: std::fmt::Display + std::fmt::Debug {} + +impl From for TransactionError +where + E: std::fmt::Display + std::fmt::Debug, +{ + fn from(e: DbErr) -> Self { + Self::Connection(e) + } +} diff --git a/sea-orm-sync/src/docs.rs b/sea-orm-sync/src/docs.rs new file mode 100644 index 0000000000..e0260f43f4 --- /dev/null +++ b/sea-orm-sync/src/docs.rs @@ -0,0 +1,172 @@ +//! 1. Async +//! +//! Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with support from day 1. +//! +//! ``` +//! # use sea_orm::{error::*, tests_cfg::*, *}; +//! # +//! # #[cfg(all(feature = "mock", not(feature = "sync")))] +//! # pub fn main() -> Result<(), DbErr> { +//! # +//! # let db = MockDatabase::new(DbBackend::Postgres) +//! # .append_query_results([ +//! # [cake::Model { +//! # id: 1, +//! # name: "New York Cheese".to_owned(), +//! # } +//! # .into_mock_row()], +//! # [fruit::Model { +//! # id: 1, +//! # name: "Apple".to_owned(), +//! # cake_id: Some(1), +//! # } +//! # .into_mock_row()], +//! # ]) +//! # .into_connection(); +//! # +//! // execute multiple queries in parallel +//! let cakes_and_fruits: (Vec, Vec) = +//! futures_util::future::try_join(Cake::find().all(&db), Fruit::find().all(&db))?; +//! # assert_eq!( +//! # cakes_and_fruits, +//! # ( +//! # vec![cake::Model { +//! # id: 1, +//! # name: "New York Cheese".to_owned(), +//! # }], +//! # vec![fruit::Model { +//! # id: 1, +//! # name: "Apple".to_owned(), +//! # cake_id: Some(1), +//! # }] +//! # ) +//! # ); +//! # assert_eq!( +//! # db.into_transaction_log(), +//! # [ +//! # Transaction::from_sql_and_values( +//! # DbBackend::Postgres, +//! # r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, +//! # [] +//! # ), +//! # Transaction::from_sql_and_values( +//! # DbBackend::Postgres, +//! # r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit""#, +//! # [] +//! # ), +//! # ] +//! # ); +//! # Ok(()) +//! # } +//! # #[cfg(all(feature = "mock", feature = "sync"))] +//! # fn main() {} +//! ``` +//! +//! 2. Dynamic +//! +//! Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. +//! +//! ``` +//! # use sea_query::Query; +//! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*}; +//! # fn function(db: DbConn) -> Result<(), DbErr> { +//! // build subquery with ease +//! let cakes_with_filling: Vec = cake::Entity::find() +//! .filter( +//! Condition::any().add( +//! cake::Column::Id.in_subquery( +//! Query::select() +//! .column(cake_filling::Column::CakeId) +//! .from(cake_filling::Entity) +//! .to_owned(), +//! ), +//! ), +//! ) +//! .all(&db) +//! ?; +//! +//! # Ok(()) +//! # } +//! ``` +//! +//! 3. Testable +//! +//! Use mock connections to write unit tests for your logic. +//! +//! ``` +//! # use sea_orm::{error::*, entity::*, query::*, tests_cfg::*, DbConn, MockDatabase, Transaction, DbBackend}; +//! # fn function(db: DbConn) -> Result<(), DbErr> { +//! // Setup mock connection +//! let db = MockDatabase::new(DbBackend::Postgres) +//! .append_query_results([ +//! [ +//! cake::Model { +//! id: 1, +//! name: "New York Cheese".to_owned(), +//! }, +//! ], +//! ]) +//! .into_connection(); +//! +//! // Perform your application logic +//! assert_eq!( +//! cake::Entity::find().one(&db)?, +//! Some(cake::Model { +//! id: 1, +//! name: "New York Cheese".to_owned(), +//! }) +//! ); +//! +//! // Compare it against the expected transaction log +//! assert_eq!( +//! db.into_transaction_log(), +//! [ +//! Transaction::from_sql_and_values( +//! DbBackend::Postgres, +//! r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, +//! [1u64.into()] +//! ), +//! ] +//! ); +//! # Ok(()) +//! # } +//! ``` +//! +//! 4. Service Oriented +//! +//! Quickly build services that join, filter, sort and paginate data in APIs. +//! +//! ```ignore +//! #[get("/?&")] +//! fn list( +//! conn: Connection, +//! page: Option, +//! per_page: Option, +//! ) -> Template { +//! // Set page number and items per page +//! let page = page.unwrap_or(1); +//! let per_page = per_page.unwrap_or(10); +//! +//! // Setup paginator +//! let paginator = Post::find() +//! .order_by_asc(post::Column::Id) +//! .paginate(&conn, per_page); +//! let num_pages = paginator.num_pages().unwrap(); +//! +//! // Fetch paginated posts +//! let posts = paginator +//! .fetch_page(page - 1) +//! +//! .expect("could not retrieve posts"); +//! +//! Template::render( +//! "index", +//! context! { +//! page: page, +//! per_page: per_page, +//! posts: posts, +//! num_pages: num_pages, +//! }, +//! ) +//! } +//! ``` diff --git a/sea-orm-sync/src/driver/mock.rs b/sea-orm-sync/src/driver/mock.rs new file mode 100644 index 0000000000..4057cdf123 --- /dev/null +++ b/sea-orm-sync/src/driver/mock.rs @@ -0,0 +1,278 @@ +use crate::{ + DatabaseConnection, DatabaseConnectionType, DbBackend, ExecResult, MockDatabase, QueryResult, + Statement, Transaction, debug_print, error::*, +}; +use std::{ + fmt::Debug, + pin::Pin, + sync::{ + Arc, Mutex, + atomic::{AtomicUsize, Ordering}, + }, +}; +use tracing::instrument; + +#[cfg(not(feature = "sync"))] +type PinBoxStream = Pin>>>; +#[cfg(feature = "sync")] +type PinBoxStream = Box>>; + +/// Defines a database driver for the [MockDatabase] +#[derive(Debug)] +pub struct MockDatabaseConnector; + +/// Defines a connection for the [MockDatabase] +#[derive(Debug)] +pub struct MockDatabaseConnection { + execute_counter: AtomicUsize, + query_counter: AtomicUsize, + mocker: Mutex>, +} + +/// A Trait for any type wanting to perform operations on the [MockDatabase] +pub trait MockDatabaseTrait: Debug { + /// Execute a statement in the [MockDatabase] + fn execute(&mut self, counter: usize, stmt: Statement) -> Result; + + /// Execute a SQL query in the [MockDatabase] + fn query(&mut self, counter: usize, stmt: Statement) -> Result, DbErr>; + + /// Create a transaction that can be committed atomically + fn begin(&mut self); + + /// Commit a successful transaction atomically into the [MockDatabase] + fn commit(&mut self); + + /// Roll back a transaction since errors were encountered + fn rollback(&mut self); + + /// Get all logs from a [MockDatabase] and return a [Transaction] + fn drain_transaction_log(&mut self) -> Vec; + + /// Get the backend being used in the [MockDatabase] + fn get_database_backend(&self) -> DbBackend; + + /// Ping the [MockDatabase] + fn ping(&self) -> Result<(), DbErr>; +} + +impl MockDatabaseConnector { + /// Check if the database URI given and the [DatabaseBackend](crate::DatabaseBackend) selected are the same + #[allow(unused_variables)] + pub fn accepts(string: &str) -> bool { + #[cfg(feature = "sqlx-mysql")] + if DbBackend::MySql.is_prefix_of(string) { + return true; + } + #[cfg(feature = "sqlx-postgres")] + if DbBackend::Postgres.is_prefix_of(string) { + return true; + } + #[cfg(feature = "sqlx-sqlite")] + if DbBackend::Sqlite.is_prefix_of(string) { + return true; + } + #[cfg(feature = "rusqlite")] + if DbBackend::Sqlite.is_prefix_of(string) { + return true; + } + false + } + + /// Connect to the [MockDatabase] + #[allow(unused_variables)] + #[instrument(level = "trace")] + pub fn connect(string: &str) -> Result { + macro_rules! connect_mock_db { + ( $syntax: expr ) => { + Ok(DatabaseConnectionType::MockDatabaseConnection(Arc::new( + MockDatabaseConnection::new(MockDatabase::new($syntax)), + )) + .into()) + }; + } + + #[cfg(feature = "sqlx-mysql")] + if crate::SqlxMySqlConnector::accepts(string) { + return connect_mock_db!(DbBackend::MySql); + } + #[cfg(feature = "sqlx-postgres")] + if crate::SqlxPostgresConnector::accepts(string) { + return connect_mock_db!(DbBackend::Postgres); + } + #[cfg(feature = "sqlx-sqlite")] + if crate::SqlxSqliteConnector::accepts(string) { + return connect_mock_db!(DbBackend::Sqlite); + } + #[cfg(feature = "rusqlite")] + if crate::driver::rusqlite::RusqliteConnector::accepts(string) { + return connect_mock_db!(DbBackend::Sqlite); + } + connect_mock_db!(DbBackend::Postgres) + } +} + +impl MockDatabaseConnection { + /// Create a connection to the [MockDatabase] + pub fn new(m: M) -> Self + where + M: MockDatabaseTrait, + M: 'static, + { + Self { + execute_counter: AtomicUsize::new(0), + query_counter: AtomicUsize::new(0), + mocker: Mutex::new(Box::new(m)), + } + } + + pub(crate) fn get_mocker_mutex(&self) -> &Mutex> { + &self.mocker + } + + /// Get the [DatabaseBackend](crate::DatabaseBackend) being used by the [MockDatabase] + /// + /// # Panics + /// + /// Will panic if the lock cannot be acquired. + pub fn get_database_backend(&self) -> DbBackend { + self.mocker + .lock() + .expect("Fail to acquire mocker") + .get_database_backend() + } + + /// Execute the SQL statement in the [MockDatabase] + #[instrument(level = "trace")] + pub fn execute(&self, statement: Statement) -> Result { + debug_print!("{}", statement); + let counter = self.execute_counter.fetch_add(1, Ordering::SeqCst); + self.mocker + .lock() + .map_err(exec_err)? + .execute(counter, statement) + } + + /// Return one [QueryResult] if the query was successful + #[instrument(level = "trace")] + pub fn query_one(&self, statement: Statement) -> Result, DbErr> { + debug_print!("{}", statement); + let counter = self.query_counter.fetch_add(1, Ordering::SeqCst); + let result = self + .mocker + .lock() + .map_err(query_err)? + .query(counter, statement)?; + Ok(result.into_iter().next()) + } + + /// Return all [QueryResult]s if the query was successful + #[instrument(level = "trace")] + pub fn query_all(&self, statement: Statement) -> Result, DbErr> { + debug_print!("{}", statement); + let counter = self.query_counter.fetch_add(1, Ordering::SeqCst); + self.mocker + .lock() + .map_err(query_err)? + .query(counter, statement) + } + + /// Return [QueryResult]s from a multi-query operation + #[instrument(level = "trace")] + pub fn fetch(&self, statement: &Statement) -> PinBoxStream { + #[cfg(not(feature = "sync"))] + { + match self.query_all(statement.clone()) { + Ok(v) => Box::new(futures_util::stream::iter(v.into_iter().map(Ok))), + Err(e) => Box::new(futures_util::stream::iter(Some(Err(e)).into_iter())), + } + } + #[cfg(feature = "sync")] + { + match self.query_all(statement.clone()) { + Ok(v) => Box::new(v.into_iter().map(Ok)), + Err(e) => Box::new(Some(Err(e)).into_iter()), + } + } + } + + /// Create a statement block of SQL statements that execute together. + /// + /// # Panics + /// + /// Will panic if the lock cannot be acquired. + #[instrument(level = "trace")] + pub fn begin(&self) { + self.mocker + .lock() + .expect("Failed to acquire mocker") + .begin() + } + + /// Commit a transaction atomically to the database + /// + /// # Panics + /// + /// Will panic if the lock cannot be acquired. + #[instrument(level = "trace")] + pub fn commit(&self) { + self.mocker + .lock() + .expect("Failed to acquire mocker") + .commit() + } + + /// Roll back a faulty transaction + /// + /// # Panics + /// + /// Will panic if the lock cannot be acquired. + #[instrument(level = "trace")] + pub fn rollback(&self) { + self.mocker + .lock() + .expect("Failed to acquire mocker") + .rollback() + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + self.mocker.lock().map_err(query_err)?.ping() + } +} + +impl + From<( + Arc, + Statement, + Option, + )> for crate::QueryStream +{ + fn from( + (conn, stmt, metric_callback): ( + Arc, + Statement, + Option, + ), + ) -> Self { + crate::QueryStream::build(stmt, crate::InnerConnection::Mock(conn), metric_callback) + } +} + +impl crate::DatabaseTransaction { + pub(crate) fn new_mock( + inner: Arc, + metric_callback: Option, + ) -> Result { + use std::sync::Mutex; + let backend = inner.get_database_backend(); + Self::begin( + Arc::new(Mutex::new(crate::InnerConnection::Mock(inner))), + backend, + metric_callback, + None, + None, + None, + ) + } +} diff --git a/sea-orm-sync/src/driver/mod.rs b/sea-orm-sync/src/driver/mod.rs new file mode 100644 index 0000000000..05936460f5 --- /dev/null +++ b/sea-orm-sync/src/driver/mod.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "mock")] +mod mock; +#[cfg(feature = "proxy")] +mod proxy; +#[cfg(feature = "rusqlite")] +pub(crate) mod rusqlite; +#[cfg(any(feature = "sqlx-sqlite", feature = "rusqlite"))] +mod sqlite; +#[cfg(feature = "sqlx-dep")] +mod sqlx_common; +#[cfg(feature = "sqlx-mysql")] +pub(crate) mod sqlx_mysql; +#[cfg(feature = "sqlx-postgres")] +pub(crate) mod sqlx_postgres; +#[cfg(feature = "sqlx-sqlite")] +pub(crate) mod sqlx_sqlite; + +#[cfg(feature = "mock")] +pub use mock::*; +#[cfg(feature = "proxy")] +pub use proxy::*; +#[cfg(feature = "sqlx-dep")] +pub(crate) use sqlx_common::*; +#[cfg(feature = "sqlx-mysql")] +pub use sqlx_mysql::*; +#[cfg(feature = "sqlx-postgres")] +pub use sqlx_postgres::*; +#[cfg(feature = "sqlx-sqlite")] +pub use sqlx_sqlite::*; diff --git a/sea-orm-sync/src/driver/proxy.rs b/sea-orm-sync/src/driver/proxy.rs new file mode 100644 index 0000000000..d8e1e5e072 --- /dev/null +++ b/sea-orm-sync/src/driver/proxy.rs @@ -0,0 +1,157 @@ +use crate::{ + DatabaseConnection, DatabaseConnectionType, DbBackend, ExecResult, ProxyDatabaseTrait, + QueryResult, Statement, debug_print, error::*, +}; +use std::{fmt::Debug, sync::Arc}; +use tracing::instrument; + +/// Defines a database driver for the [ProxyDatabase] +#[derive(Debug)] +pub struct ProxyDatabaseConnector; + +/// Defines a connection for the [ProxyDatabase] +#[derive(Debug)] +pub struct ProxyDatabaseConnection { + db_backend: DbBackend, + proxy: Arc>, +} + +impl ProxyDatabaseConnector { + /// Check if the database URI given and the [DatabaseBackend](crate::DatabaseBackend) selected are the same + #[allow(unused_variables)] + pub fn accepts(string: &str) -> bool { + // As this is a proxy database, it accepts any URI + true + } + + /// Connect to the [ProxyDatabase] + #[allow(unused_variables)] + #[instrument(level = "trace")] + pub fn connect( + db_type: DbBackend, + func: Arc>, + ) -> Result { + Ok( + DatabaseConnectionType::ProxyDatabaseConnection(Arc::new( + ProxyDatabaseConnection::new(db_type, func), + )) + .into(), + ) + } +} + +impl ProxyDatabaseConnection { + /// Create a connection to the [ProxyDatabase] + pub fn new(db_backend: DbBackend, funcs: Arc>) -> Self { + Self { + db_backend, + proxy: funcs.to_owned(), + } + } + + /// Get the [DatabaseBackend](crate::DatabaseBackend) being used by the [ProxyDatabase] + pub fn get_database_backend(&self) -> DbBackend { + self.db_backend + } + + /// Execute the SQL statement in the [ProxyDatabase] + #[instrument(level = "trace")] + pub fn execute(&self, statement: Statement) -> Result { + debug_print!("{}", statement); + Ok(self.proxy.execute(statement)?.into()) + } + + /// Return one [QueryResult] if the query was successful + #[instrument(level = "trace")] + pub fn query_one(&self, statement: Statement) -> Result, DbErr> { + debug_print!("{}", statement); + let result = self.proxy.query(statement)?; + + if let Some(first) = result.first() { + return Ok(Some(QueryResult { + row: crate::QueryResultRow::Proxy(first.to_owned()), + })); + } else { + return Ok(None); + } + } + + /// Return all [QueryResult]s if the query was successful + #[instrument(level = "trace")] + pub fn query_all(&self, statement: Statement) -> Result, DbErr> { + debug_print!("{}", statement); + let result = self.proxy.query(statement)?; + + Ok(result + .into_iter() + .map(|row| QueryResult { + row: crate::QueryResultRow::Proxy(row), + }) + .collect()) + } + + /// Create a statement block of SQL statements that execute together. + #[instrument(level = "trace")] + pub fn begin(&self) { + self.proxy.begin() + } + + /// Commit a transaction atomically to the database + #[instrument(level = "trace")] + pub fn commit(&self) { + self.proxy.commit() + } + + /// Roll back a faulty transaction + #[instrument(level = "trace")] + pub fn rollback(&self) { + self.proxy.rollback() + } + + /// Start rollback a transaction + #[instrument(level = "trace")] + pub fn start_rollback(&self) { + self.proxy.start_rollback() + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + self.proxy.ping() + } +} + +impl + From<( + Arc, + Statement, + Option, + )> for crate::QueryStream +{ + fn from( + (conn, stmt, metric_callback): ( + Arc, + Statement, + Option, + ), + ) -> Self { + crate::QueryStream::build(stmt, crate::InnerConnection::Proxy(conn), metric_callback) + } +} + +impl crate::DatabaseTransaction { + pub(crate) fn new_proxy( + inner: Arc, + metric_callback: Option, + ) -> Result { + use std::sync::Mutex; + let backend = inner.get_database_backend(); + Self::begin( + Arc::new(Mutex::new(crate::InnerConnection::Proxy(inner))), + backend, + metric_callback, + None, + None, + None, + ) + } +} diff --git a/sea-orm-sync/src/driver/rusqlite.rs b/sea-orm-sync/src/driver/rusqlite.rs new file mode 100644 index 0000000000..42d6ba54b4 --- /dev/null +++ b/sea-orm-sync/src/driver/rusqlite.rs @@ -0,0 +1,732 @@ +use std::{ + ops::Deref, + sync::{Arc, Mutex, MutexGuard, TryLockError}, + time::{Duration, Instant}, +}; +use tracing::{debug, instrument, warn}; + +pub use OwnedRow as RusqliteRow; +use rusqlite::{ + CachedStatement, OpenFlags, Row, + types::{FromSql, FromSqlError, Value}, +}; +pub use rusqlite::{ + Connection as RusqliteConnection, Error as RusqliteError, types::Value as RusqliteOwnedValue, +}; +use sea_query_rusqlite::{RusqliteValue, RusqliteValues, rusqlite}; + +use crate::{ + AccessMode, ColIdx, ConnectOptions, DatabaseConnection, DatabaseConnectionType, + DatabaseTransaction, InnerConnection, IsolationLevel, QueryStream, SqliteTransactionMode, + Statement, TransactionError, error::*, executor::*, +}; + +/// A helper class to connect to Rusqlite +#[derive(Debug)] +pub struct RusqliteConnector; + +const DEFAULT_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60); + +/// A shared SQLite connection for synchronous contexts. +/// +/// Unlike sqlx's connection pool, which maintains multiple connections that can +/// be checked out concurrently by different tasks, this holds a single +/// [`RusqliteConnection`] behind an `Arc>`. All callers contend on +/// the same mutex; [`acquire`](Self::acquire) spins with `try_lock` until the +/// mutex is available or the `acquire_timeout` deadline expires. +/// +/// The connection can also be *loaned* out (via [`State::Loaned`]) for the +/// duration of a transaction or streaming query, during which `acquire` will +/// keep retrying until the loan is returned. +#[derive(Clone)] +pub struct RusqliteSharedConnection { + pub(crate) conn: Arc>, + acquire_timeout: Duration, + metric_callback: Option, +} + +/// A loaned connection that supports nested transactions. +/// +/// Created by [`RusqliteSharedConnection::loan`], which moves the underlying +/// [`RusqliteConnection`] out of the shared mutex (leaving it in +/// [`State::Loaned`]) so that a transaction or streaming query can hold it +/// without blocking other callers on the mutex itself - they will still wait +/// via `acquire`, but the mutex is not held for the entire duration. +/// +/// On [`Drop`], the connection is returned to the shared mutex so that +/// subsequent callers can acquire it again. +/// +/// ## Transaction semantics +/// +/// Nested transactions follow the same model as sqlx's SQLite driver: +/// +/// | Depth | `begin` | `commit` | `rollback` | +/// |-------|--------------------|-----------------------|-------------------------| +/// | 0 | `BEGIN` | - | - | +/// | 1 | `SAVEPOINT sp1` | `COMMIT` | `ROLLBACK` | +/// | 2 | `SAVEPOINT sp2` | `RELEASE SAVEPOINT sp1` | `ROLLBACK TO sp1` | +/// | *n* | `SAVEPOINT sp`*n* | `RELEASE SAVEPOINT sp`*n-1* | `ROLLBACK TO sp`*n-1* | +/// +/// ## Comparison with rusqlite's native `Transaction` +/// +/// rusqlite's [`rusqlite::Transaction`] borrows `&mut Connection` for its +/// entire lifetime. This makes it difficult to pass around, store in structs, +/// or nest - each nested [`rusqlite::Savepoint`] borrows `&mut Transaction`, +/// creating a tower of coupled lifetimes that the borrow checker enforces +/// strictly. +/// +/// Here, the connection is *moved* (loaned) into this struct as owned state, +/// and nesting is tracked with a simple `transaction_depth` counter. This +/// means transactions and savepoints can be started, committed, or rolled back +/// through the same flat API without lifetime gymnastics. The connection is +/// returned to the shared pool on [`Drop`], rather than relying on the borrow +/// ending. +pub struct RusqliteInnerConnection { + conn: State, + loan: Arc>, + transaction_depth: u32, +} + +#[derive(Debug)] +pub struct RusqliteExecResult { + pub(crate) rows_affected: u64, + pub(crate) last_insert_rowid: i64, +} + +#[derive(Debug)] +pub struct OwnedRow { + pub columns: Vec>, + pub values: Vec, +} + +#[derive(Debug, Default)] +pub enum State { + Idle(RusqliteConnection), + Loaned, + #[default] + Disconnected, +} + +impl OwnedRow { + pub fn columns(&self) -> &[Arc] { + &self.columns + } + + pub fn from_row(columns: Vec>, row: &Row) -> OwnedRow { + let mut values = Vec::new(); + + for i in 0..columns.len() { + let v: Value = row.get_unwrap(i); + values.push(v); + } + + OwnedRow { columns, values } + } + + pub fn try_get(&self, idx: I) -> Result { + let (idx, col, value) = if let Some(idx) = idx.as_usize() { + (*idx, None, &self.values[*idx]) + } else if let Some(name) = idx.as_str() { + if let Some(idx) = self.columns.iter().position(|c| c.deref() == name) { + (idx, Some(name), &self.values[idx]) + } else { + return Err(TryGetError::Null(format!( + "column `{name}` does not exist in row" + ))); + } + } else { + unreachable!("ColIdx must be either usize or str") + }; + FromSql::column_result(value.into()) + .map_err(|err| match err { + FromSqlError::OutOfRange(i) => RusqliteError::IntegralValueOutOfRange(idx, i), + FromSqlError::Other(err) => { + RusqliteError::FromSqlConversionFailure(idx, value.data_type(), err) + } + FromSqlError::InvalidBlobSize { .. } => { + RusqliteError::FromSqlConversionFailure(idx, value.data_type(), Box::new(err)) + } + // FromSqlError::InvalidType + _ => RusqliteError::InvalidColumnType( + idx, + col.map(|c| c.to_owned()).unwrap_or_default(), + value.data_type(), + ), + }) + .map_err(|err| TryGetError::DbErr(query_err(err))) + } +} + +impl std::fmt::Debug for RusqliteSharedConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RusqliteSharedConnection {{ conn: {:?} }}", self.conn) + } +} + +impl std::fmt::Debug for RusqliteInnerConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RusqliteInnerConnection {{ conn: {:?} }}", self.conn) + } +} + +impl From for RusqliteSharedConnection { + fn from(conn: RusqliteConnection) -> Self { + RusqliteSharedConnection { + conn: Arc::new(Mutex::new(State::Idle(conn))), + acquire_timeout: DEFAULT_ACQUIRE_TIMEOUT, + metric_callback: None, + } + } +} + +impl From for DatabaseConnection { + fn from(conn: RusqliteSharedConnection) -> Self { + DatabaseConnectionType::RusqliteSharedConnection(conn).into() + } +} + +impl RusqliteConnector { + #[cfg(feature = "mock")] + /// Check if the URI provided corresponds to `sqlite:` for a SQLite database + pub fn accepts(string: &str) -> bool { + string.starts_with("sqlite:") + } + + /// Add configuration options for the SQLite database + #[instrument(level = "trace")] + pub fn connect(options: ConnectOptions) -> Result { + let acquire_timeout = options.acquire_timeout.unwrap_or(DEFAULT_ACQUIRE_TIMEOUT); + // TODO handle disable_statement_logging + let after_conn = options.after_connect; + + let raw = options + .url + .trim_start_matches("sqlite://") + .trim_start_matches("sqlite:"); + + let (path, mode) = match raw.find('?') { + Some(q) => { + let query = &raw[q + 1..]; + let mut mode = None; + for kv in query.split('&') { + if let Some(val) = kv.strip_prefix("mode=") { + mode = Some(val); + } else if !kv.is_empty() { + return Err(DbErr::Conn(RuntimeErr::Internal(format!( + "unsupported SQLite connection parameter: {kv}" + )))); + } + } + (&raw[..q], mode) + } + None => (raw, None), + }; + + let conn = match mode { + None | Some("rwc") => RusqliteConnection::open(path), + Some("ro") => RusqliteConnection::open_with_flags( + path, + OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX, + ), + Some("rw") => RusqliteConnection::open_with_flags( + path, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX, + ), + Some(other) => { + return Err(DbErr::Conn(RuntimeErr::Internal(format!( + "unknown SQLite mode: {other}" + )))); + } + } + .map_err(conn_err)?; + + let conn = RusqliteSharedConnection { + conn: Arc::new(Mutex::new(State::Idle(conn))), + acquire_timeout, + metric_callback: None, + }; + + #[cfg(feature = "sqlite-use-returning-for-3_35")] + { + let version = get_version(&conn)?; + super::sqlite::ensure_returning_version(&version)?; + } + + // SQLx also enables this by default + conn.execute_unprepared("PRAGMA foreign_keys = ON")?; + let conn: DatabaseConnection = conn.into(); + + if let Some(cb) = after_conn { + cb(conn.clone())?; + } + + Ok(conn) + } +} + +// impl RusqliteConnector { +// /// Convert a Rusqlite connection to a [DatabaseConnection] +// pub fn from_rusqlite_connection(conn: RusqliteConnection) -> DatabaseConnection { +// let conn: RusqliteSharedConnection = conn.into(); +// conn.into() +// } +// } + +impl RusqliteSharedConnection { + pub fn acquire(&self) -> Result, DbErr> { + let deadline = Instant::now() + self.acquire_timeout; + loop { + match self.conn.try_lock() { + Ok(state) => match *state { + State::Idle(_) => return Ok(state), + State::Loaned => (), // borrowed for streaming or transaction + State::Disconnected => { + return Err(DbErr::ConnectionAcquire(ConnAcquireErr::ConnectionClosed)); + } + }, + Err(TryLockError::WouldBlock) => (), + Err(TryLockError::Poisoned(_)) => { + return Err(DbErr::ConnectionAcquire(ConnAcquireErr::ConnectionClosed)); + } + } + if Instant::now() >= deadline { + return Err(DbErr::ConnectionAcquire(ConnAcquireErr::Timeout)); + } + std::thread::yield_now(); + } + } + + fn loan(&self) -> Result { + let conn = { + let mut conn = self.acquire()?; + conn.loan() + }; + Ok(RusqliteInnerConnection { + conn: State::Idle(conn), + loan: self.conn.clone(), + transaction_depth: 0, + }) + } + + /// Execute a [Statement] on a SQLite backend + #[instrument(level = "trace")] + pub fn execute(&self, stmt: Statement) -> Result { + debug!("{}", stmt); + + let values = sql_values(&stmt); + let conn = self.acquire()?; + let conn = conn.conn(); + crate::metric::metric!(self.metric_callback, &stmt, { + match conn.execute(&stmt.sql, &*values.as_params()) { + Ok(rows_affected) => Ok(RusqliteExecResult { + rows_affected: rows_affected as u64, + last_insert_rowid: conn.last_insert_rowid(), + } + .into()), + Err(err) => Err(exec_err(err)), + } + }) + } + + /// Execute an unprepared SQL statement on a SQLite backend + #[instrument(level = "trace")] + pub fn execute_unprepared(&self, sql: &str) -> Result { + debug!("{}", sql); + + let conn = self.acquire()?; + let conn = conn.conn(); + match conn.execute_batch(sql) { + Ok(()) => Ok(RusqliteExecResult { + rows_affected: conn.changes(), + last_insert_rowid: conn.last_insert_rowid(), + } + .into()), + Err(err) => Err(exec_err(err)), + } + } + + /// Get one result from a SQL query. Returns [Option::None] if no match was found + #[instrument(level = "trace")] + pub fn query_one(&self, stmt: Statement) -> Result, DbErr> { + debug!("{}", stmt); + + let values = sql_values(&stmt); + let conn = self.acquire()?; + let conn = conn.conn(); + let mut sql = conn.prepare_cached(&stmt.sql).map_err(query_err)?; + let columns: Vec> = column_names(&sql); + + crate::metric::metric!(self.metric_callback, &stmt, { + match sql.query(&*values.as_params()) { + Ok(mut rows) => { + let mut out = None; + if let Some(row) = rows.next().map_err(query_err)? { + out = Some(OwnedRow::from_row(columns.clone(), row).into()); + } + Ok(out) + } + Err(err) => Err(query_err(err)), + } + }) + } + + /// Get the results of a query returning them as a Vec<[QueryResult]> + #[instrument(level = "trace")] + pub fn query_all(&self, stmt: Statement) -> Result, DbErr> { + debug!("{}", stmt); + + let values = sql_values(&stmt); + let conn = self.acquire()?; + let conn = conn.conn(); + let mut sql = conn.prepare_cached(&stmt.sql).map_err(query_err)?; + let columns: Vec> = column_names(&sql); + + crate::metric::metric!(self.metric_callback, &stmt, { + match sql.query(&*values.as_params()) { + Ok(mut rows) => { + let mut out = Vec::new(); + while let Some(row) = rows.next().map_err(query_err)? { + out.push(OwnedRow::from_row(columns.clone(), row).into()); + } + Ok(out) + } + Err(err) => Err(query_err(err)), + } + }) + } + + /// Stream the results of executing a SQL query + #[instrument(level = "trace")] + pub fn stream(&self, stmt: Statement) -> Result { + debug!("{}", stmt); + + Ok(QueryStream::build( + stmt, + InnerConnection::Rusqlite(self.loan()?), + self.metric_callback.clone(), + )) + } + + /// Bundle a set of SQL statements that execute together. + #[instrument(level = "trace")] + pub fn begin( + &self, + isolation_level: Option, + access_mode: Option, + sqlite_transaction_mode: Option, + ) -> Result { + let conn = self.loan()?; + DatabaseTransaction::begin( + Arc::new(Mutex::new(InnerConnection::Rusqlite(conn))), + crate::DbBackend::Sqlite, + self.metric_callback.clone(), + isolation_level, + access_mode, + sqlite_transaction_mode, + ) + } + + /// Create a SQLite transaction + #[instrument(level = "trace", skip(callback))] + pub fn transaction( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'b> FnOnce(&'b DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + self.begin(isolation_level, access_mode, None) + .map_err(|e| TransactionError::Connection(e))? + .run(callback) + } + + pub(crate) fn set_metric_callback(&mut self, callback: F) + where + F: Fn(&crate::metric::Info<'_>) + 'static, + { + self.metric_callback = Some(Arc::new(callback)); + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + let conn = self.acquire()?; + let conn = conn.conn(); + let mut stmt = conn.prepare("SELECT 1").map_err(conn_err)?; + match stmt.query([]) { + Ok(_) => Ok(()), + Err(err) => Err(conn_err(err)), + } + } + + /// Explicitly close the SQLite connection. + /// See [`Self::close_by_ref`] for usage with references. + pub fn close(self) -> Result<(), DbErr> { + self.close_by_ref() + } + + /// Explicitly close the SQLite connection + pub fn close_by_ref(&self) -> Result<(), DbErr> { + let mut conn = self.acquire()?; + *conn = State::Disconnected; + Ok(()) + } +} + +impl RusqliteInnerConnection { + #[instrument(level = "trace", skip(metric_callback))] + pub fn execute( + &self, + stmt: Statement, + metric_callback: &Option, + ) -> Result { + debug!("{}", stmt); + + let values = sql_values(&stmt); + let conn = self.conn.conn(); + crate::metric::metric!(metric_callback, &stmt, { + match conn.execute(&stmt.sql, &*values.as_params()) { + Ok(rows_affected) => Ok(RusqliteExecResult { + rows_affected: rows_affected as u64, + last_insert_rowid: conn.last_insert_rowid(), + } + .into()), + Err(err) => Err(exec_err(err)), + } + }) + } + + #[instrument(level = "trace")] + pub(crate) fn execute_unprepared(&self, sql: &str) -> Result { + debug!("{}", sql); + + let conn = self.conn.conn(); + match conn.execute_batch(sql) { + Ok(()) => Ok(RusqliteExecResult { + rows_affected: conn.changes(), + last_insert_rowid: conn.last_insert_rowid(), + } + .into()), + Err(err) => Err(exec_err(err)), + } + } + + #[instrument(level = "trace", skip(metric_callback))] + pub fn query_one( + &self, + stmt: Statement, + metric_callback: &Option, + ) -> Result, DbErr> { + debug!("{}", stmt); + + let values = sql_values(&stmt); + let conn = self.conn.conn(); + let mut sql = conn.prepare_cached(&stmt.sql).map_err(query_err)?; + let columns: Vec> = column_names(&sql); + + crate::metric::metric!(metric_callback, &stmt, { + match sql.query(&*values.as_params()) { + Ok(mut rows) => { + let mut out = None; + if let Some(row) = rows.next().map_err(query_err)? { + out = Some(OwnedRow::from_row(columns.clone(), row).into()); + } + Ok(out) + } + Err(err) => Err(query_err(err)), + } + }) + } + + #[instrument(level = "trace", skip(metric_callback))] + pub fn query_all( + &self, + stmt: Statement, + metric_callback: &Option, + ) -> Result, DbErr> { + debug!("{}", stmt); + + let values = sql_values(&stmt); + let conn = self.conn.conn(); + let mut sql = conn.prepare_cached(&stmt.sql).map_err(query_err)?; + let columns: Vec> = column_names(&sql); + + crate::metric::metric!(metric_callback, &stmt, { + match sql.query(&*values.as_params()) { + Ok(mut rows) => { + let mut out = Vec::new(); + while let Some(row) = rows.next().map_err(query_err)? { + out.push(OwnedRow::from_row(columns.clone(), row).into()); + } + Ok(out) + } + Err(err) => Err(query_err(err)), + } + }) + } + + #[instrument(level = "trace")] + pub(crate) fn stream(&self, stmt: &Statement) -> Result, DbErr> { + debug!("{}", stmt); + + let values = sql_values(stmt); + let conn = self.conn.conn(); + let mut sql = conn.prepare_cached(&stmt.sql).map_err(query_err)?; + let columns: Vec> = column_names(&sql); + + let rows = match sql.query(&*values.as_params()) { + Ok(mut rows) => { + let mut out = Vec::new(); + while let Some(row) = rows.next().map_err(query_err)? { + out.push(OwnedRow::from_row(columns.clone(), row).into()); + } + out + } + Err(err) => return Err(query_err(err)), + }; + + Ok(rows) + } + + #[instrument(level = "trace")] + pub(crate) fn begin( + &mut self, + sqlite_transaction_mode: Option, + ) -> Result<(), DbErr> { + if self.transaction_depth == 0 { + match sqlite_transaction_mode { + Some(mode) => { + self.execute_unprepared(&format!("BEGIN {}", mode.sqlite_keyword()))? + } + None => self.execute_unprepared("BEGIN")?, + }; + } else { + self.execute_unprepared(&format!("SAVEPOINT sp{}", self.transaction_depth))?; + } + self.transaction_depth += 1; + Ok(()) + } + + #[instrument(level = "trace")] + pub(crate) fn commit(&mut self) -> Result<(), DbErr> { + if self.transaction_depth == 1 { + self.execute_unprepared("COMMIT")?; + } else { + self.execute_unprepared(&format!( + "RELEASE SAVEPOINT sp{}", + self.transaction_depth - 1 + ))?; + } + self.transaction_depth -= 1; + Ok(()) + } + + #[instrument(level = "trace")] + pub(crate) fn rollback(&mut self) -> Result<(), DbErr> { + if self.transaction_depth == 1 { + self.execute_unprepared("ROLLBACK")?; + } else { + self.execute_unprepared(&format!("ROLLBACK TO sp{}", self.transaction_depth - 1))?; + } + self.transaction_depth -= 1; + Ok(()) + } + + #[instrument(level = "trace")] + pub(crate) fn start_rollback(&mut self) -> Result<(), DbErr> { + if self.transaction_depth > 0 { + self.rollback()?; + } + Ok(()) + } +} + +impl Drop for RusqliteInnerConnection { + fn drop(&mut self) { + let mut loan = self.loan.lock().unwrap(); + loan.return_(self.conn.loan()); + } +} + +impl State { + fn conn(&self) -> &RusqliteConnection { + match self { + State::Idle(conn) => conn, + _ => panic!("No connection"), + } + } + + fn loan(&mut self) -> RusqliteConnection { + let mut conn = State::Loaned; + std::mem::swap(&mut conn, self); + match conn { + State::Idle(conn) => conn, + _ => panic!("No connection"), + } + } + + fn return_(&mut self, conn: RusqliteConnection) { + *self = State::Idle(conn); + } +} + +impl From for QueryResult { + fn from(row: OwnedRow) -> QueryResult { + QueryResult { + row: QueryResultRow::Rusqlite(row), + } + } +} + +impl From for ExecResult { + fn from(result: RusqliteExecResult) -> ExecResult { + ExecResult { + result: ExecResultHolder::Rusqlite(result), + } + } +} + +pub(crate) fn sql_values(stmt: &Statement) -> RusqliteValues { + let values = match &stmt.values { + Some(values) => values.iter().cloned().map(RusqliteValue).collect(), + None => Vec::new(), + }; + RusqliteValues(values) +} + +fn column_names(sql: &CachedStatement) -> Vec> { + sql.column_names() + .into_iter() + .map(|r| Arc::from(r)) + .collect() +} + +#[cfg(feature = "sqlite-use-returning-for-3_35")] +fn get_version(conn: &RusqliteSharedConnection) -> Result { + let stmt = Statement { + sql: "SELECT sqlite_version()".to_string(), + values: None, + db_backend: crate::DbBackend::Sqlite, + }; + conn.query_one(stmt)? + .ok_or_else(|| { + DbErr::Conn(RuntimeErr::Internal( + "Error reading SQLite version".to_string(), + )) + })? + .try_get_by(0) +} + +fn conn_err(err: RusqliteError) -> DbErr { + DbErr::Conn(RuntimeErr::Rusqlite(err.into())) +} + +fn exec_err(err: RusqliteError) -> DbErr { + DbErr::Exec(RuntimeErr::Rusqlite(err.into())) +} + +fn query_err(err: RusqliteError) -> DbErr { + DbErr::Query(RuntimeErr::Rusqlite(err.into())) +} diff --git a/sea-orm-sync/src/driver/sqlite.rs b/sea-orm-sync/src/driver/sqlite.rs new file mode 100644 index 0000000000..ee09ec1e51 --- /dev/null +++ b/sea-orm-sync/src/driver/sqlite.rs @@ -0,0 +1,63 @@ +use crate::error::{DbErr, RuntimeErr}; + +#[cfg(feature = "sqlite-use-returning-for-3_35")] +pub fn ensure_returning_version(version: &str) -> Result<(), DbErr> { + let mut parts = version.trim().split('.').map(|part| { + part.parse::().map_err(|_| { + DbErr::Conn(RuntimeErr::Internal( + "Error parsing SQLite version".to_string(), + )) + }) + }); + + let mut extract_next = || { + parts.next().transpose().and_then(|part| { + part.ok_or_else(|| { + DbErr::Conn(RuntimeErr::Internal("SQLite version too short".to_string())) + }) + }) + }; + + let major = extract_next()?; + let minor = extract_next()?; + + if major > 3 || (major == 3 && minor >= 35) { + Ok(()) + } else { + Err(DbErr::BackendNotSupported { + db: "SQLite", + ctx: "SQLite version does not support returning", + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "sqlite-use-returning-for-3_35")] + #[test] + fn test_ensure_returning_version() { + assert!(ensure_returning_version("").is_err()); + assert!(ensure_returning_version(".").is_err()); + assert!(ensure_returning_version(".a").is_err()); + assert!(ensure_returning_version(".4.9").is_err()); + assert!(ensure_returning_version("a").is_err()); + assert!(ensure_returning_version("1.").is_err()); + assert!(ensure_returning_version("1.a").is_err()); + + assert!(ensure_returning_version("1.1").is_err()); + assert!(ensure_returning_version("1.0.").is_err()); + assert!(ensure_returning_version("1.0.0").is_err()); + assert!(ensure_returning_version("2.0.0").is_err()); + assert!(ensure_returning_version("3.34.0").is_err()); + assert!(ensure_returning_version("3.34.999").is_err()); + + // valid version + assert!(ensure_returning_version("3.35.0").is_ok()); + assert!(ensure_returning_version("3.35.1").is_ok()); + assert!(ensure_returning_version("3.36.0").is_ok()); + assert!(ensure_returning_version("4.0.0").is_ok()); + assert!(ensure_returning_version("99.0.0").is_ok()); + } +} diff --git a/sea-orm-sync/src/driver/sqlx_common.rs b/sea-orm-sync/src/driver/sqlx_common.rs new file mode 100644 index 0000000000..6090dfd644 --- /dev/null +++ b/sea-orm-sync/src/driver/sqlx_common.rs @@ -0,0 +1,66 @@ +use crate::{ConnAcquireErr, ConnectOptions, DbErr, RuntimeErr}; + +/// Converts an [sqlx::error] execution error to a [DbErr] +pub fn sqlx_error_to_exec_err(err: sqlx::Error) -> DbErr { + DbErr::Exec(RuntimeErr::SqlxError(err.into())) +} + +/// Converts an [sqlx::error] query error to a [DbErr] +pub fn sqlx_error_to_query_err(err: sqlx::Error) -> DbErr { + DbErr::Query(RuntimeErr::SqlxError(err.into())) +} + +/// Converts an [sqlx::error] connection error to a [DbErr] +pub fn sqlx_error_to_conn_err(err: sqlx::Error) -> DbErr { + DbErr::Conn(RuntimeErr::SqlxError(err.into())) +} + +/// Converts an [sqlx::error] error to a [DbErr] +pub fn sqlx_map_err_ignore_not_found( + err: Result, sqlx::Error>, +) -> Result, DbErr> { + if let Err(sqlx::Error::RowNotFound) = err { + Ok(None) + } else { + err.map_err(sqlx_error_to_query_err) + } +} + +/// Converts an [sqlx::error] error to a [DbErr] +pub fn sqlx_conn_acquire_err(sqlx_err: sqlx::Error) -> DbErr { + match sqlx_err { + sqlx::Error::PoolTimedOut => DbErr::ConnectionAcquire(ConnAcquireErr::Timeout), + sqlx::Error::PoolClosed => DbErr::ConnectionAcquire(ConnAcquireErr::ConnectionClosed), + _ => DbErr::Conn(RuntimeErr::SqlxError(sqlx_err.into())), + } +} + +impl ConnectOptions { + /// Convert [ConnectOptions] into [sqlx::pool::PoolOptions] + pub fn sqlx_pool_options(self) -> sqlx::pool::PoolOptions + where + DB: sqlx::Database, + { + let mut opt = sqlx::pool::PoolOptions::new(); + if let Some(max_connections) = self.max_connections { + opt = opt.max_connections(max_connections); + } + if let Some(min_connections) = self.min_connections { + opt = opt.min_connections(min_connections); + } + if let Some(connect_timeout) = self.connect_timeout { + opt = opt.acquire_timeout(connect_timeout); + } + if let Some(idle_timeout) = self.idle_timeout { + opt = opt.idle_timeout(idle_timeout); + } + if let Some(acquire_timeout) = self.acquire_timeout { + opt = opt.acquire_timeout(acquire_timeout); + } + if let Some(max_lifetime) = self.max_lifetime { + opt = opt.max_lifetime(max_lifetime); + } + opt = opt.test_before_acquire(self.test_before_acquire); + opt + } +} diff --git a/sea-orm-sync/src/driver/sqlx_mysql.rs b/sea-orm-sync/src/driver/sqlx_mysql.rs new file mode 100644 index 0000000000..0faf306f73 --- /dev/null +++ b/sea-orm-sync/src/driver/sqlx_mysql.rs @@ -0,0 +1,520 @@ +use log::LevelFilter; +use sea_query::Values; +use std::sync::Arc; +use std::sync::Mutex; + +use sqlx::{ + Connection, Executor, MySql, MySqlPool, + mysql::{MySqlConnectOptions, MySqlQueryResult, MySqlRow}, + pool::PoolConnection, +}; + +use sea_query_sqlx::SqlxValues; +use tracing::instrument; + +use crate::{ + AccessMode, ConnectOptions, DatabaseConnection, DatabaseConnectionType, DatabaseTransaction, + DbBackend, IsolationLevel, QueryStream, Statement, TransactionError, debug_print, error::*, + executor::*, +}; + +use super::sqlx_common::*; + +/// Defines the [sqlx::mysql] connector +#[derive(Debug)] +pub struct SqlxMySqlConnector; + +/// Defines a sqlx MySQL pool +#[derive(Clone)] +pub struct SqlxMySqlPoolConnection { + pub(crate) pool: MySqlPool, + metric_callback: Option, +} + +impl std::fmt::Debug for SqlxMySqlPoolConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SqlxMySqlPoolConnection {{ pool: {:?} }}", self.pool) + } +} + +impl From for SqlxMySqlPoolConnection { + fn from(pool: MySqlPool) -> Self { + SqlxMySqlPoolConnection { + pool, + metric_callback: None, + } + } +} + +impl From for DatabaseConnection { + fn from(pool: MySqlPool) -> Self { + DatabaseConnectionType::SqlxMySqlPoolConnection(pool.into()).into() + } +} + +impl SqlxMySqlConnector { + /// Check if the URI provided corresponds to `mysql://` for a MySQL database + pub fn accepts(string: &str) -> bool { + string.starts_with("mysql://") && string.parse::().is_ok() + } + + /// Add configuration options for the MySQL database + #[instrument(level = "trace")] + pub fn connect(options: ConnectOptions) -> Result { + let mut sqlx_opts = options + .url + .parse::() + .map_err(sqlx_error_to_conn_err)?; + use sqlx::ConnectOptions; + if !options.sqlx_logging { + sqlx_opts = sqlx_opts.disable_statement_logging(); + } else { + sqlx_opts = sqlx_opts.log_statements(options.sqlx_logging_level); + if options.sqlx_slow_statements_logging_level != LevelFilter::Off { + sqlx_opts = sqlx_opts.log_slow_statements( + options.sqlx_slow_statements_logging_level, + options.sqlx_slow_statements_logging_threshold, + ); + } + } + + if let Some(f) = &options.mysql_opts_fn { + sqlx_opts = f(sqlx_opts); + } + + let after_connect = options.after_connect.clone(); + + let pool = if options.connect_lazy { + options.sqlx_pool_options().connect_lazy_with(sqlx_opts) + } else { + options + .sqlx_pool_options() + .connect_with(sqlx_opts) + .map_err(sqlx_error_to_conn_err)? + }; + + let conn: DatabaseConnection = + DatabaseConnectionType::SqlxMySqlPoolConnection(SqlxMySqlPoolConnection { + pool, + metric_callback: None, + }) + .into(); + + if let Some(cb) = after_connect { + cb(conn.clone())?; + } + + Ok(conn) + } +} + +impl SqlxMySqlConnector { + /// Instantiate a sqlx pool connection to a [DatabaseConnection] + pub fn from_sqlx_mysql_pool(pool: MySqlPool) -> DatabaseConnection { + DatabaseConnectionType::SqlxMySqlPoolConnection(SqlxMySqlPoolConnection { + pool, + metric_callback: None, + }) + .into() + } +} + +impl SqlxMySqlPoolConnection { + /// Execute a [Statement] on a MySQL backend + #[instrument(level = "trace")] + pub fn execute(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.execute(&mut *conn) { + Ok(res) => Ok(res.into()), + Err(err) => Err(sqlx_error_to_exec_err(err)), + } + }) + } + + /// Execute an unprepared SQL statement on a MySQL backend + #[instrument(level = "trace")] + pub fn execute_unprepared(&self, sql: &str) -> Result { + debug_print!("{}", sql); + + let conn = &mut self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + match conn.execute(sql) { + Ok(res) => Ok(res.into()), + Err(err) => Err(sqlx_error_to_exec_err(err)), + } + } + + /// Get one result from a SQL query. Returns [Option::None] if no match was found + #[instrument(level = "trace")] + pub fn query_one(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.fetch_one(&mut *conn) { + Ok(row) => Ok(Some(row.into())), + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(None), + _ => Err(sqlx_error_to_query_err(err)), + }, + } + }) + } + + /// Get the results of a query returning them as a Vec<[QueryResult]> + #[instrument(level = "trace")] + pub fn query_all(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.fetch_all(&mut *conn) { + Ok(rows) => Ok(rows.into_iter().map(|r| r.into()).collect()), + Err(err) => Err(sqlx_error_to_query_err(err)), + } + }) + } + + /// Stream the results of executing a SQL query + #[instrument(level = "trace")] + pub fn stream(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + Ok(QueryStream::from(( + conn, + stmt, + self.metric_callback.clone(), + ))) + } + + /// Bundle a set of SQL statements that execute together. + #[instrument(level = "trace")] + pub fn begin( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + DatabaseTransaction::new_mysql( + conn, + self.metric_callback.clone(), + isolation_level, + access_mode, + ) + } + + /// Create a MySQL transaction + #[instrument(level = "trace", skip(callback))] + pub fn transaction( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'b> FnOnce(&'b DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + let transaction = DatabaseTransaction::new_mysql( + conn, + self.metric_callback.clone(), + isolation_level, + access_mode, + ) + .map_err(|e| TransactionError::Connection(e))?; + transaction.run(callback) + } + + pub(crate) fn set_metric_callback(&mut self, callback: F) + where + F: Fn(&crate::metric::Info<'_>) + 'static, + { + self.metric_callback = Some(Arc::new(callback)); + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + let conn = &mut self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + match conn.ping() { + Ok(_) => Ok(()), + Err(err) => Err(sqlx_error_to_conn_err(err)), + } + } + + /// Explicitly close the MySQL connection. + /// See [`Self::close_by_ref`] for usage with references. + pub fn close(self) -> Result<(), DbErr> { + self.close_by_ref() + } + + /// Explicitly close the MySQL connection + pub fn close_by_ref(&self) -> Result<(), DbErr> { + self.pool.close(); + Ok(()) + } +} + +impl From for QueryResult { + fn from(row: MySqlRow) -> QueryResult { + QueryResult { + row: QueryResultRow::SqlxMySql(row), + } + } +} + +impl From for ExecResult { + fn from(result: MySqlQueryResult) -> ExecResult { + ExecResult { + result: ExecResultHolder::SqlxMySql(result), + } + } +} + +pub(crate) fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, MySql, SqlxValues> { + let values = stmt + .values + .as_ref() + .map_or(Values(Vec::new()), |values| values.clone()); + sqlx::query_with(&stmt.sql, SqlxValues(values)) +} + +pub(crate) fn set_transaction_config( + conn: &mut PoolConnection, + isolation_level: Option, + access_mode: Option, +) -> Result<(), DbErr> { + let mut settings = Vec::new(); + + if let Some(isolation_level) = isolation_level { + settings.push(format!("ISOLATION LEVEL {isolation_level}")); + } + + if let Some(access_mode) = access_mode { + settings.push(access_mode.to_string()); + } + + if !settings.is_empty() { + let stmt = Statement { + sql: format!("SET TRANSACTION {}", settings.join(", ")), + values: None, + db_backend: DbBackend::MySql, + }; + let query = sqlx_query(&stmt); + conn.execute(query).map_err(sqlx_error_to_exec_err)?; + } + Ok(()) +} + +impl + From<( + PoolConnection, + Statement, + Option, + )> for crate::QueryStream +{ + fn from( + (conn, stmt, metric_callback): ( + PoolConnection, + Statement, + Option, + ), + ) -> Self { + crate::QueryStream::build(stmt, crate::InnerConnection::MySql(conn), metric_callback) + } +} + +impl crate::DatabaseTransaction { + pub(crate) fn new_mysql( + inner: PoolConnection, + metric_callback: Option, + isolation_level: Option, + access_mode: Option, + ) -> Result { + Self::begin( + Arc::new(Mutex::new(crate::InnerConnection::MySql(inner))), + crate::DbBackend::MySql, + metric_callback, + isolation_level, + access_mode, + None, + ) + } +} + +#[cfg(feature = "proxy")] +pub(crate) fn from_sqlx_mysql_row_to_proxy_row(row: &sqlx::mysql::MySqlRow) -> crate::ProxyRow { + // https://docs.rs/sqlx-mysql/0.7.2/src/sqlx_mysql/protocol/text/column.rs.html + // https://docs.rs/sqlx-mysql/0.7.2/sqlx_mysql/types/index.html + use sea_query::Value; + use sqlx::{Column, Row, TypeInfo}; + crate::ProxyRow { + values: row + .columns() + .iter() + .map(|c| { + ( + c.name().to_string(), + match c.type_info().name() { + "TINYINT(1)" | "BOOLEAN" => { + Value::Bool(row.try_get(c.ordinal()).expect("Failed to get boolean")) + } + "TINYINT UNSIGNED" => Value::TinyUnsigned( + row.try_get(c.ordinal()) + .expect("Failed to get unsigned tiny integer"), + ), + "SMALLINT UNSIGNED" => Value::SmallUnsigned( + row.try_get(c.ordinal()) + .expect("Failed to get unsigned small integer"), + ), + "INT UNSIGNED" => Value::Unsigned( + row.try_get(c.ordinal()) + .expect("Failed to get unsigned integer"), + ), + "MEDIUMINT UNSIGNED" | "BIGINT UNSIGNED" => Value::BigUnsigned( + row.try_get(c.ordinal()) + .expect("Failed to get unsigned big integer"), + ), + "TINYINT" => Value::TinyInt( + row.try_get(c.ordinal()) + .expect("Failed to get tiny integer"), + ), + "SMALLINT" => Value::SmallInt( + row.try_get(c.ordinal()) + .expect("Failed to get small integer"), + ), + "INT" => { + Value::Int(row.try_get(c.ordinal()).expect("Failed to get integer")) + } + "MEDIUMINT" | "BIGINT" => Value::BigInt( + row.try_get(c.ordinal()).expect("Failed to get big integer"), + ), + "FLOAT" => { + Value::Float(row.try_get(c.ordinal()).expect("Failed to get float")) + } + "DOUBLE" => { + Value::Double(row.try_get(c.ordinal()).expect("Failed to get double")) + } + + "BIT" | "BINARY" | "VARBINARY" | "TINYBLOB" | "BLOB" | "MEDIUMBLOB" + | "LONGBLOB" => Value::Bytes( + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get bytes") + .map(Box::new), + ), + + "CHAR" | "VARCHAR" | "TINYTEXT" | "TEXT" | "MEDIUMTEXT" | "LONGTEXT" => { + Value::String( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get string") + .map(Box::new), + ) + } + + #[cfg(feature = "with-chrono")] + "TIMESTAMP" => Value::ChronoDateTimeUtc( + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timestamp") + .map(Box::new), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIMESTAMP" => Value::TimeDateTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get timestamp") + .map(Box::new), + ), + + #[cfg(feature = "with-chrono")] + "DATE" => Value::ChronoDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get date") + .map(Box::new), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "DATE" => Value::TimeDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get date") + .map(Box::new), + ), + + #[cfg(feature = "with-chrono")] + "TIME" => Value::ChronoTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get time") + .map(Box::new), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIME" => Value::TimeTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get time") + .map(Box::new), + ), + + #[cfg(feature = "with-chrono")] + "DATETIME" => Value::ChronoDateTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get datetime") + .map(Box::new), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "DATETIME" => Value::TimeDateTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get datetime") + .map(Box::new), + ), + + #[cfg(feature = "with-chrono")] + "YEAR" => Value::ChronoDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get year") + .map(Box::new), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "YEAR" => Value::TimeDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get year") + .map(Box::new), + ), + + "ENUM" | "SET" | "GEOMETRY" => Value::String( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get serialized string") + .map(Box::new), + ), + + #[cfg(feature = "with-bigdecimal")] + "DECIMAL" => Value::BigDecimal( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get decimal") + .map(Box::new), + ), + #[cfg(all( + feature = "with-rust_decimal", + not(feature = "with-bigdecimal") + ))] + "DECIMAL" => Value::Decimal( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get decimal") + .map(Box::new), + ), + + #[cfg(feature = "with-json")] + "JSON" => Value::Json( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get json") + .map(Box::new), + ), + + _ => unreachable!("Unknown column type: {}", c.type_info().name()), + }, + ) + }) + .collect(), + } +} diff --git a/sea-orm-sync/src/driver/sqlx_postgres.rs b/sea-orm-sync/src/driver/sqlx_postgres.rs new file mode 100644 index 0000000000..06ed6e128a --- /dev/null +++ b/sea-orm-sync/src/driver/sqlx_postgres.rs @@ -0,0 +1,910 @@ +use log::LevelFilter; +use sea_query::Values; +use std::sync::Mutex; +use std::{fmt::Write, sync::Arc}; + +use sqlx::{ + Connection, Executor, PgPool, Postgres, + pool::PoolConnection, + postgres::{PgConnectOptions, PgQueryResult, PgRow}, +}; + +use sea_query_sqlx::SqlxValues; +use tracing::instrument; + +use crate::{ + AccessMode, ConnectOptions, DatabaseConnection, DatabaseConnectionType, DatabaseTransaction, + IsolationLevel, QueryStream, Statement, TransactionError, debug_print, error::*, executor::*, +}; + +use super::sqlx_common::*; + +/// Defines the [sqlx::postgres] connector +#[derive(Debug)] +pub struct SqlxPostgresConnector; + +/// Defines a sqlx PostgreSQL pool +#[derive(Clone)] +pub struct SqlxPostgresPoolConnection { + pub(crate) pool: PgPool, + metric_callback: Option, +} + +impl std::fmt::Debug for SqlxPostgresPoolConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SqlxPostgresPoolConnection {{ pool: {:?} }}", self.pool) + } +} + +impl From for SqlxPostgresPoolConnection { + fn from(pool: PgPool) -> Self { + SqlxPostgresPoolConnection { + pool, + metric_callback: None, + } + } +} + +impl From for DatabaseConnection { + fn from(pool: PgPool) -> Self { + DatabaseConnectionType::SqlxPostgresPoolConnection(pool.into()).into() + } +} + +impl SqlxPostgresConnector { + /// Check if the URI provided corresponds to `postgres://` for a PostgreSQL database + pub fn accepts(string: &str) -> bool { + string.starts_with("postgres://") && string.parse::().is_ok() + } + + /// Add configuration options for the PostgreSQL database + #[instrument(level = "trace")] + pub fn connect(options: ConnectOptions) -> Result { + let mut sqlx_opts = options + .url + .parse::() + .map_err(sqlx_error_to_conn_err)?; + use sqlx::ConnectOptions; + if !options.sqlx_logging { + sqlx_opts = sqlx_opts.disable_statement_logging(); + } else { + sqlx_opts = sqlx_opts.log_statements(options.sqlx_logging_level); + if options.sqlx_slow_statements_logging_level != LevelFilter::Off { + sqlx_opts = sqlx_opts.log_slow_statements( + options.sqlx_slow_statements_logging_level, + options.sqlx_slow_statements_logging_threshold, + ); + } + } + + if let Some(application_name) = &options.application_name { + sqlx_opts = sqlx_opts.application_name(application_name); + } + + if let Some(timeout) = options.statement_timeout { + sqlx_opts = sqlx_opts.options([("statement_timeout", timeout.as_millis().to_string())]); + } + + if let Some(f) = &options.pg_opts_fn { + sqlx_opts = f(sqlx_opts); + } + + let set_search_path_sql = options.schema_search_path.as_ref().map(|schema| { + let mut string = "SET search_path = ".to_owned(); + if schema.starts_with('"') { + write!(&mut string, "{schema}").expect("Infallible"); + } else { + for (i, schema) in schema.split(',').enumerate() { + if i > 0 { + write!(&mut string, ",").expect("Infallible"); + } + if schema.starts_with('"') { + write!(&mut string, "{schema}").expect("Infallible"); + } else { + write!(&mut string, "\"{schema}\"").expect("Infallible"); + } + } + } + string + }); + + let lazy = options.connect_lazy; + let after_connect = options.after_connect.clone(); + let mut pool_options = options.sqlx_pool_options(); + + if let Some(sql) = set_search_path_sql { + pool_options = pool_options.after_connect(move |conn, _| { + let sql = sql.clone(); + ({ sqlx::Executor::execute(conn, sql.as_str()).map(|_| ()) }) + }); + } + + let pool = if lazy { + pool_options.connect_lazy_with(sqlx_opts) + } else { + pool_options + .connect_with(sqlx_opts) + .map_err(sqlx_error_to_conn_err)? + }; + + let conn: DatabaseConnection = + DatabaseConnectionType::SqlxPostgresPoolConnection(SqlxPostgresPoolConnection { + pool, + metric_callback: None, + }) + .into(); + + if let Some(cb) = after_connect { + cb(conn.clone())?; + } + + Ok(conn) + } +} + +impl SqlxPostgresConnector { + /// Instantiate a sqlx pool connection to a [DatabaseConnection] + pub fn from_sqlx_postgres_pool(pool: PgPool) -> DatabaseConnection { + DatabaseConnectionType::SqlxPostgresPoolConnection(SqlxPostgresPoolConnection { + pool, + metric_callback: None, + }) + .into() + } +} + +impl SqlxPostgresPoolConnection { + /// Execute a [Statement] on a PostgreSQL backend + #[instrument(level = "trace")] + pub fn execute(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.execute(&mut *conn) { + Ok(res) => Ok(res.into()), + Err(err) => Err(sqlx_error_to_exec_err(err)), + } + }) + } + + /// Execute an unprepared SQL statement on a PostgreSQL backend + #[instrument(level = "trace")] + pub fn execute_unprepared(&self, sql: &str) -> Result { + debug_print!("{}", sql); + + let conn = &mut self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + match conn.execute(sql) { + Ok(res) => Ok(res.into()), + Err(err) => Err(sqlx_error_to_exec_err(err)), + } + } + + /// Get one result from a SQL query. Returns [Option::None] if no match was found + #[instrument(level = "trace")] + pub fn query_one(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.fetch_one(&mut *conn) { + Ok(row) => Ok(Some(row.into())), + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(None), + _ => Err(sqlx_error_to_query_err(err)), + }, + } + }) + } + + /// Get the results of a query returning them as a Vec<[QueryResult]> + #[instrument(level = "trace")] + pub fn query_all(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.fetch_all(&mut *conn) { + Ok(rows) => Ok(rows.into_iter().map(|r| r.into()).collect()), + Err(err) => Err(sqlx_error_to_query_err(err)), + } + }) + } + + /// Stream the results of executing a SQL query + #[instrument(level = "trace")] + pub fn stream(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + Ok(QueryStream::from(( + conn, + stmt, + self.metric_callback.clone(), + ))) + } + + /// Bundle a set of SQL statements that execute together. + #[instrument(level = "trace")] + pub fn begin( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + DatabaseTransaction::new_postgres( + conn, + self.metric_callback.clone(), + isolation_level, + access_mode, + ) + } + + /// Create a PostgreSQL transaction + #[instrument(level = "trace", skip(callback))] + pub fn transaction( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'b> FnOnce(&'b DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + let transaction = DatabaseTransaction::new_postgres( + conn, + self.metric_callback.clone(), + isolation_level, + access_mode, + ) + .map_err(|e| TransactionError::Connection(e))?; + transaction.run(callback) + } + + pub(crate) fn set_metric_callback(&mut self, callback: F) + where + F: Fn(&crate::metric::Info<'_>) + 'static, + { + self.metric_callback = Some(Arc::new(callback)); + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + let conn = &mut self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + match conn.ping() { + Ok(_) => Ok(()), + Err(err) => Err(sqlx_error_to_conn_err(err)), + } + } + + /// Explicitly close the Postgres connection. + /// See [`Self::close_by_ref`] for usage with references. + pub fn close(self) -> Result<(), DbErr> { + self.close_by_ref() + } + + /// Explicitly close the Postgres connection + pub fn close_by_ref(&self) -> Result<(), DbErr> { + self.pool.close(); + Ok(()) + } +} + +impl From for QueryResult { + fn from(row: PgRow) -> QueryResult { + QueryResult { + row: QueryResultRow::SqlxPostgres(row), + } + } +} + +impl From for ExecResult { + fn from(result: PgQueryResult) -> ExecResult { + ExecResult { + result: ExecResultHolder::SqlxPostgres(result), + } + } +} + +pub(crate) fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Postgres, SqlxValues> { + let values = stmt + .values + .as_ref() + .map_or(Values(Vec::new()), |values| values.clone()); + sqlx::query_with(&stmt.sql, SqlxValues(values)) +} + +pub(crate) fn set_transaction_config( + conn: &mut PoolConnection, + isolation_level: Option, + access_mode: Option, +) -> Result<(), DbErr> { + let mut settings = Vec::new(); + + if let Some(isolation_level) = isolation_level { + settings.push(format!("ISOLATION LEVEL {isolation_level}")); + } + + if let Some(access_mode) = access_mode { + settings.push(access_mode.to_string()); + } + + if !settings.is_empty() { + let sql = format!("SET TRANSACTION {}", settings.join(" ")); + sqlx::query(&sql) + .execute(&mut **conn) + .map_err(sqlx_error_to_exec_err)?; + } + Ok(()) +} + +impl + From<( + PoolConnection, + Statement, + Option, + )> for crate::QueryStream +{ + fn from( + (conn, stmt, metric_callback): ( + PoolConnection, + Statement, + Option, + ), + ) -> Self { + crate::QueryStream::build( + stmt, + crate::InnerConnection::Postgres(conn), + metric_callback, + ) + } +} + +impl crate::DatabaseTransaction { + pub(crate) fn new_postgres( + inner: PoolConnection, + metric_callback: Option, + isolation_level: Option, + access_mode: Option, + ) -> Result { + Self::begin( + Arc::new(Mutex::new(crate::InnerConnection::Postgres(inner))), + crate::DbBackend::Postgres, + metric_callback, + isolation_level, + access_mode, + None, + ) + } +} + +#[cfg(feature = "proxy")] +pub(crate) fn from_sqlx_postgres_row_to_proxy_row(row: &sqlx::postgres::PgRow) -> crate::ProxyRow { + // https://docs.rs/sqlx-postgres/0.7.2/src/sqlx_postgres/type_info.rs.html + // https://docs.rs/sqlx-postgres/0.7.2/sqlx_postgres/types/index.html + use sea_query::Value; + use sqlx::{Column, Row, TypeInfo}; + crate::ProxyRow { + values: row + .columns() + .iter() + .map(|c| { + ( + c.name().to_string(), + match c.type_info().name() { + "BOOL" => { + Value::Bool(row.try_get(c.ordinal()).expect("Failed to get boolean")) + } + #[cfg(feature = "postgres-array")] + "BOOL[]" => Value::Array( + sea_query::ArrayType::Bool, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get boolean array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Bool(Some(val))) + .collect(), + ) + }), + ), + + "\"CHAR\"" => Value::TinyInt( + row.try_get(c.ordinal()) + .expect("Failed to get small integer"), + ), + #[cfg(feature = "postgres-array")] + "\"CHAR\"[]" => Value::Array( + sea_query::ArrayType::TinyInt, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get small integer array") + .map(|vals: Vec| { + Box::new( + vals.into_iter() + .map(|val| Value::TinyInt(Some(val))) + .collect(), + ) + }), + ), + + "SMALLINT" | "SMALLSERIAL" | "INT2" => Value::SmallInt( + row.try_get(c.ordinal()) + .expect("Failed to get small integer"), + ), + #[cfg(feature = "postgres-array")] + "SMALLINT[]" | "SMALLSERIAL[]" | "INT2[]" => Value::Array( + sea_query::ArrayType::SmallInt, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get small integer array") + .map(|vals: Vec| { + Box::new( + vals.into_iter() + .map(|val| Value::SmallInt(Some(val))) + .collect(), + ) + }), + ), + + "INT" | "SERIAL" | "INT4" => { + Value::Int(row.try_get(c.ordinal()).expect("Failed to get integer")) + } + #[cfg(feature = "postgres-array")] + "INT[]" | "SERIAL[]" | "INT4[]" => Value::Array( + sea_query::ArrayType::Int, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get integer array") + .map(|vals: Vec| { + Box::new( + vals.into_iter().map(|val| Value::Int(Some(val))).collect(), + ) + }), + ), + + "BIGINT" | "BIGSERIAL" | "INT8" => Value::BigInt( + row.try_get(c.ordinal()).expect("Failed to get big integer"), + ), + #[cfg(feature = "postgres-array")] + "BIGINT[]" | "BIGSERIAL[]" | "INT8[]" => Value::Array( + sea_query::ArrayType::BigInt, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get big integer array") + .map(|vals: Vec| { + Box::new( + vals.into_iter() + .map(|val| Value::BigInt(Some(val))) + .collect(), + ) + }), + ), + + "FLOAT4" | "REAL" => { + Value::Float(row.try_get(c.ordinal()).expect("Failed to get float")) + } + #[cfg(feature = "postgres-array")] + "FLOAT4[]" | "REAL[]" => Value::Array( + sea_query::ArrayType::Float, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get float array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Float(Some(val))) + .collect(), + ) + }), + ), + + "FLOAT8" | "DOUBLE PRECISION" => { + Value::Double(row.try_get(c.ordinal()).expect("Failed to get double")) + } + #[cfg(feature = "postgres-array")] + "FLOAT8[]" | "DOUBLE PRECISION[]" => Value::Array( + sea_query::ArrayType::Double, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get double array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Double(Some(val))) + .collect(), + ) + }), + ), + + "VARCHAR" | "CHAR" | "TEXT" | "NAME" => Value::String( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get string"), + ), + #[cfg(feature = "postgres-array")] + "VARCHAR[]" | "CHAR[]" | "TEXT[]" | "NAME[]" => Value::Array( + sea_query::ArrayType::String, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get string array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::String(Some(val))) + .collect(), + ) + }), + ), + + "BYTEA" => Value::Bytes( + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get bytes"), + ), + #[cfg(feature = "postgres-array")] + "BYTEA[]" => Value::Array( + sea_query::ArrayType::Bytes, + row.try_get::>>, _>(c.ordinal()) + .expect("Failed to get bytes array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Bytes(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-bigdecimal")] + "NUMERIC" => Value::BigDecimal( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get numeric"), + ), + #[cfg(all( + feature = "with-rust_decimal", + not(feature = "with-bigdecimal") + ))] + "NUMERIC" => { + Value::Decimal(row.try_get(c.ordinal()).expect("Failed to get numeric")) + } + + #[cfg(all(feature = "with-bigdecimal", feature = "postgres-array"))] + "NUMERIC[]" => Value::Array( + sea_query::ArrayType::BigDecimal, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get numeric array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::BigDecimal(Some(val))) + .collect(), + ) + }), + ), + #[cfg(all( + feature = "with-rust_decimal", + not(feature = "with-bigdecimal"), + feature = "postgres-array" + ))] + "NUMERIC[]" => Value::Array( + sea_query::ArrayType::Decimal, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get numeric array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Decimal(Some(val))) + .collect(), + ) + }), + ), + + "OID" => { + Value::BigInt(row.try_get(c.ordinal()).expect("Failed to get oid")) + } + #[cfg(feature = "postgres-array")] + "OID[]" => Value::Array( + sea_query::ArrayType::BigInt, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get oid array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::BigInt(Some(val))) + .collect(), + ) + }), + ), + + "JSON" | "JSONB" => Value::Json( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get json") + .map(Box::new), + ), + #[cfg(any(feature = "json-array", feature = "postgres-array"))] + "JSON[]" | "JSONB[]" => Value::Array( + sea_query::ArrayType::Json, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get json array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Json(Some(Box::new(val)))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-ipnetwork")] + "INET" | "CIDR" => Value::IpNetwork( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get ip address"), + ), + #[cfg(feature = "with-ipnetwork")] + "INET[]" | "CIDR[]" => Value::Array( + sea_query::ArrayType::IpNetwork, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get ip address array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::IpNetwork(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-mac_address")] + "MACADDR" | "MACADDR8" => Value::MacAddress( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get mac address"), + ), + #[cfg(all(feature = "with-mac_address", feature = "postgres-array"))] + "MACADDR[]" | "MACADDR8[]" => Value::Array( + sea_query::ArrayType::MacAddress, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get mac address array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::MacAddress(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-chrono")] + "TIMESTAMP" => Value::ChronoDateTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get timestamp"), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIMESTAMP" => Value::TimeDateTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get timestamp"), + ), + + #[cfg(all(feature = "with-chrono", feature = "postgres-array"))] + "TIMESTAMP[]" => Value::Array( + sea_query::ArrayType::ChronoDateTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timestamp array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::ChronoDateTime(Some(val))) + .collect(), + ) + }), + ), + #[cfg(all( + feature = "with-time", + not(feature = "with-chrono"), + feature = "postgres-array" + ))] + "TIMESTAMP[]" => Value::Array( + sea_query::ArrayType::TimeDateTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timestamp array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::TimeDateTime(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-chrono")] + "DATE" => Value::ChronoDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get date"), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "DATE" => Value::TimeDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get date"), + ), + + #[cfg(all(feature = "with-chrono", feature = "postgres-array"))] + "DATE[]" => Value::Array( + sea_query::ArrayType::ChronoDate, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get date array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::ChronoDate(Some(val))) + .collect(), + ) + }), + ), + #[cfg(all( + feature = "with-time", + not(feature = "with-chrono"), + feature = "postgres-array" + ))] + "DATE[]" => Value::Array( + sea_query::ArrayType::TimeDate, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get date array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::TimeDate(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-chrono")] + "TIME" => Value::ChronoTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get time"), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIME" => Value::TimeTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get time"), + ), + + #[cfg(all(feature = "with-chrono", feature = "postgres-array"))] + "TIME[]" => Value::Array( + sea_query::ArrayType::ChronoTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get time array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::ChronoTime(Some(val))) + .collect(), + ) + }), + ), + #[cfg(all( + feature = "with-time", + not(feature = "with-chrono"), + feature = "postgres-array" + ))] + "TIME[]" => Value::Array( + sea_query::ArrayType::TimeTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get time array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::TimeTime(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-chrono")] + "TIMESTAMPTZ" => Value::ChronoDateTimeUtc( + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timestamptz"), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIMESTAMPTZ" => Value::TimeDateTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get timestamptz"), + ), + + #[cfg(all(feature = "with-chrono", feature = "postgres-array"))] + "TIMESTAMPTZ[]" => Value::Array( + sea_query::ArrayType::ChronoDateTimeUtc, + row.try_get::>>, _>( + c.ordinal(), + ) + .expect("Failed to get timestamptz array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::ChronoDateTimeUtc(Some(val))) + .collect(), + ) + }), + ), + #[cfg(all( + feature = "with-time", + not(feature = "with-chrono"), + feature = "postgres-array" + ))] + "TIMESTAMPTZ[]" => Value::Array( + sea_query::ArrayType::TimeDateTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timestamptz array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::TimeDateTime(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-chrono")] + "TIMETZ" => Value::ChronoTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get timetz"), + ), + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIMETZ" => { + Value::TimeTime(row.try_get(c.ordinal()).expect("Failed to get timetz")) + } + + #[cfg(all(feature = "with-chrono", feature = "postgres-array"))] + "TIMETZ[]" => Value::Array( + sea_query::ArrayType::ChronoTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timetz array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::ChronoTime(Some(val))) + .collect(), + ) + }), + ), + #[cfg(all( + feature = "with-time", + not(feature = "with-chrono"), + feature = "postgres-array" + ))] + "TIMETZ[]" => Value::Array( + sea_query::ArrayType::TimeTime, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timetz array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::TimeTime(Some(val))) + .collect(), + ) + }), + ), + + #[cfg(feature = "with-uuid")] + "UUID" => Value::Uuid( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get uuid"), + ), + + #[cfg(all(feature = "with-uuid", feature = "postgres-array"))] + "UUID[]" => Value::Array( + sea_query::ArrayType::Uuid, + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get uuid array") + .map(|vals| { + Box::new( + vals.into_iter() + .map(|val| Value::Uuid(Some(val))) + .collect(), + ) + }), + ), + + _ => unreachable!("Unknown column type: {}", c.type_info().name()), + }, + ) + }) + .collect(), + } +} diff --git a/sea-orm-sync/src/driver/sqlx_sqlite.rs b/sea-orm-sync/src/driver/sqlx_sqlite.rs new file mode 100644 index 0000000000..8d9456fddc --- /dev/null +++ b/sea-orm-sync/src/driver/sqlx_sqlite.rs @@ -0,0 +1,478 @@ +use log::LevelFilter; +use sea_query::Values; +use std::sync::Arc; +use std::sync::Mutex; + +use sqlx::{ + Connection, Executor, Sqlite, SqlitePool, + pool::PoolConnection, + sqlite::{SqliteConnectOptions, SqliteQueryResult, SqliteRow}, +}; + +use sea_query_sqlx::SqlxValues; +use tracing::{instrument, warn}; + +use crate::{ + AccessMode, ConnectOptions, DatabaseConnection, DatabaseConnectionType, DatabaseTransaction, + IsolationLevel, QueryStream, SqliteTransactionMode, Statement, TransactionError, debug_print, + error::*, executor::*, sqlx_error_to_exec_err, +}; + +use super::sqlx_common::*; + +/// Defines the [sqlx::sqlite] connector +#[derive(Debug)] +pub struct SqlxSqliteConnector; + +/// Defines a sqlx SQLite pool +#[derive(Clone)] +pub struct SqlxSqlitePoolConnection { + pub(crate) pool: SqlitePool, + metric_callback: Option, +} + +impl std::fmt::Debug for SqlxSqlitePoolConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SqlxSqlitePoolConnection {{ pool: {:?} }}", self.pool) + } +} + +impl From for SqlxSqlitePoolConnection { + fn from(pool: SqlitePool) -> Self { + SqlxSqlitePoolConnection { + pool, + metric_callback: None, + } + } +} + +impl From for DatabaseConnection { + fn from(pool: SqlitePool) -> Self { + DatabaseConnectionType::SqlxSqlitePoolConnection(pool.into()).into() + } +} + +impl SqlxSqliteConnector { + /// Check if the URI provided corresponds to `sqlite:` for a SQLite database + pub fn accepts(string: &str) -> bool { + string.starts_with("sqlite:") && string.parse::().is_ok() + } + + /// Add configuration options for the SQLite database + #[instrument(level = "trace")] + pub fn connect(options: ConnectOptions) -> Result { + let mut options = options; + let mut sqlx_opts = options + .url + .parse::() + .map_err(sqlx_error_to_conn_err)?; + if let Some(sqlcipher_key) = &options.sqlcipher_key { + sqlx_opts = sqlx_opts.pragma("key", sqlcipher_key.clone()); + } + use sqlx::ConnectOptions; + if !options.sqlx_logging { + sqlx_opts = sqlx_opts.disable_statement_logging(); + } else { + sqlx_opts = sqlx_opts.log_statements(options.sqlx_logging_level); + if options.sqlx_slow_statements_logging_level != LevelFilter::Off { + sqlx_opts = sqlx_opts.log_slow_statements( + options.sqlx_slow_statements_logging_level, + options.sqlx_slow_statements_logging_threshold, + ); + } + } + + if options.get_max_connections().is_none() { + options.max_connections(1); + } + + if let Some(f) = &options.sqlite_opts_fn { + sqlx_opts = f(sqlx_opts); + } + + let after_conn = options.after_connect.clone(); + + let pool = if options.connect_lazy { + options.sqlx_pool_options().connect_lazy_with(sqlx_opts) + } else { + options + .sqlx_pool_options() + .connect_with(sqlx_opts) + .map_err(sqlx_error_to_conn_err)? + }; + + let pool = SqlxSqlitePoolConnection { + pool, + metric_callback: None, + }; + + #[cfg(feature = "sqlite-use-returning-for-3_35")] + { + let version = get_version(&pool)?; + super::sqlite::ensure_returning_version(&version)?; + } + + let conn: DatabaseConnection = + DatabaseConnectionType::SqlxSqlitePoolConnection(pool).into(); + + if let Some(cb) = after_conn { + cb(conn.clone())?; + } + + Ok(conn) + } +} + +impl SqlxSqliteConnector { + /// Instantiate a sqlx pool connection to a [DatabaseConnection] + pub fn from_sqlx_sqlite_pool(pool: SqlitePool) -> DatabaseConnection { + DatabaseConnectionType::SqlxSqlitePoolConnection(SqlxSqlitePoolConnection { + pool, + metric_callback: None, + }) + .into() + } +} + +impl SqlxSqlitePoolConnection { + /// Execute a [Statement] on a SQLite backend + #[instrument(level = "trace")] + pub fn execute(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.execute(&mut *conn) { + Ok(res) => Ok(res.into()), + Err(err) => Err(sqlx_error_to_exec_err(err)), + } + }) + } + + /// Execute an unprepared SQL statement on a SQLite backend + #[instrument(level = "trace")] + pub fn execute_unprepared(&self, sql: &str) -> Result { + debug_print!("{}", sql); + + let conn = &mut self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + match conn.execute(sql) { + Ok(res) => Ok(res.into()), + Err(err) => Err(sqlx_error_to_exec_err(err)), + } + } + + /// Get one result from a SQL query. Returns [Option::None] if no match was found + #[instrument(level = "trace")] + pub fn query_one(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.fetch_one(&mut *conn) { + Ok(row) => Ok(Some(row.into())), + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(None), + _ => Err(sqlx_error_to_query_err(err)), + }, + } + }) + } + + /// Get the results of a query returning them as a Vec<[QueryResult]> + #[instrument(level = "trace")] + pub fn query_all(&self, stmt: Statement) -> Result, DbErr> { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + let mut conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + crate::metric::metric!(self.metric_callback, &stmt, { + match query.fetch_all(&mut *conn) { + Ok(rows) => Ok(rows.into_iter().map(|r| r.into()).collect()), + Err(err) => Err(sqlx_error_to_query_err(err)), + } + }) + } + + /// Stream the results of executing a SQL query + #[instrument(level = "trace")] + pub fn stream(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + Ok(QueryStream::from(( + conn, + stmt, + self.metric_callback.clone(), + ))) + } + + /// Bundle a set of SQL statements that execute together. + #[instrument(level = "trace")] + pub fn begin( + &self, + isolation_level: Option, + access_mode: Option, + sqlite_transaction_mode: Option, + ) -> Result { + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + DatabaseTransaction::new_sqlite( + conn, + self.metric_callback.clone(), + isolation_level, + access_mode, + sqlite_transaction_mode, + ) + } + + /// Create a SQLite transaction + #[instrument(level = "trace", skip(callback))] + pub fn transaction( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'b> FnOnce(&'b DatabaseTransaction) -> Result, + E: std::fmt::Display + std::fmt::Debug, + { + let conn = self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + let transaction = DatabaseTransaction::new_sqlite( + conn, + self.metric_callback.clone(), + isolation_level, + access_mode, + None, + ) + .map_err(|e| TransactionError::Connection(e))?; + transaction.run(callback) + } + + pub(crate) fn set_metric_callback(&mut self, callback: F) + where + F: Fn(&crate::metric::Info<'_>) + 'static, + { + self.metric_callback = Some(Arc::new(callback)); + } + + /// Checks if a connection to the database is still valid. + pub fn ping(&self) -> Result<(), DbErr> { + let conn = &mut self.pool.acquire().map_err(sqlx_conn_acquire_err)?; + match conn.ping() { + Ok(_) => Ok(()), + Err(err) => Err(sqlx_error_to_conn_err(err)), + } + } + + /// Explicitly close the SQLite connection. + /// See [`Self::close_by_ref`] for usage with references. + pub fn close(self) -> Result<(), DbErr> { + self.close_by_ref() + } + + /// Explicitly close the SQLite connection + pub fn close_by_ref(&self) -> Result<(), DbErr> { + self.pool.close(); + Ok(()) + } +} + +impl From for QueryResult { + fn from(row: SqliteRow) -> QueryResult { + QueryResult { + row: QueryResultRow::SqlxSqlite(row), + } + } +} + +impl From for ExecResult { + fn from(result: SqliteQueryResult) -> ExecResult { + ExecResult { + result: ExecResultHolder::SqlxSqlite(result), + } + } +} + +pub(crate) fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Sqlite, SqlxValues> { + let values = stmt + .values + .as_ref() + .map_or(Values(Vec::new()), |values| values.clone()); + sqlx::query_with(&stmt.sql, SqlxValues(values)) +} + +pub(crate) fn set_transaction_config( + _conn: &mut PoolConnection, + isolation_level: Option, + access_mode: Option, +) -> Result<(), DbErr> { + if isolation_level.is_some() { + warn!("Setting isolation level in a SQLite transaction isn't supported"); + } + if access_mode.is_some() { + warn!("Setting access mode in a SQLite transaction isn't supported"); + } + Ok(()) +} + +#[cfg(feature = "sqlite-use-returning-for-3_35")] +fn get_version(conn: &SqlxSqlitePoolConnection) -> Result { + let stmt = Statement { + sql: "SELECT sqlite_version()".to_string(), + values: None, + db_backend: crate::DbBackend::Sqlite, + }; + conn.query_one(stmt)? + .ok_or_else(|| { + DbErr::Conn(RuntimeErr::Internal( + "Error reading SQLite version".to_string(), + )) + })? + .try_get_by(0) +} + +impl + From<( + PoolConnection, + Statement, + Option, + )> for crate::QueryStream +{ + fn from( + (conn, stmt, metric_callback): ( + PoolConnection, + Statement, + Option, + ), + ) -> Self { + crate::QueryStream::build(stmt, crate::InnerConnection::Sqlite(conn), metric_callback) + } +} + +impl crate::DatabaseTransaction { + pub(crate) fn new_sqlite( + inner: PoolConnection, + metric_callback: Option, + isolation_level: Option, + access_mode: Option, + sqlite_transaction_mode: Option, + ) -> Result { + Self::begin( + Arc::new(Mutex::new(crate::InnerConnection::Sqlite(inner))), + crate::DbBackend::Sqlite, + metric_callback, + isolation_level, + access_mode, + sqlite_transaction_mode, + ) + } +} + +#[cfg(feature = "proxy")] +pub(crate) fn from_sqlx_sqlite_row_to_proxy_row(row: &sqlx::sqlite::SqliteRow) -> crate::ProxyRow { + // https://docs.rs/sqlx-sqlite/0.7.2/src/sqlx_sqlite/type_info.rs.html + // https://docs.rs/sqlx-sqlite/0.7.2/sqlx_sqlite/types/index.html + use sea_query::Value; + use sqlx::{Column, Row, TypeInfo}; + crate::ProxyRow { + values: row + .columns() + .iter() + .map(|c| { + ( + c.name().to_string(), + match c.type_info().name() { + "BOOLEAN" => { + Value::Bool(row.try_get(c.ordinal()).expect("Failed to get boolean")) + } + + "INTEGER" => { + Value::Int(row.try_get(c.ordinal()).expect("Failed to get integer")) + } + + "BIGINT" | "INT8" => Value::BigInt( + row.try_get(c.ordinal()).expect("Failed to get big integer"), + ), + + "REAL" => { + Value::Double(row.try_get(c.ordinal()).expect("Failed to get double")) + } + + "TEXT" => Value::String( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get string") + .map(Box::new), + ), + + "BLOB" => Value::Bytes( + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get bytes") + .map(Box::new), + ), + + #[cfg(feature = "with-chrono")] + "DATETIME" => { + use chrono::{DateTime, Utc}; + + Value::ChronoDateTimeUtc( + row.try_get::>, _>(c.ordinal()) + .expect("Failed to get timestamp") + .map(Box::new), + ) + } + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "DATETIME" => { + use time::OffsetDateTime; + Value::TimeDateTimeWithTimeZone( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get timestamp") + .map(Box::new), + ) + } + #[cfg(feature = "with-chrono")] + "DATE" => { + use chrono::NaiveDate; + Value::ChronoDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get date") + .map(Box::new), + ) + } + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "DATE" => { + use time::Date; + Value::TimeDate( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get date") + .map(Box::new), + ) + } + + #[cfg(feature = "with-chrono")] + "TIME" => { + use chrono::NaiveTime; + Value::ChronoTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get time") + .map(Box::new), + ) + } + #[cfg(all(feature = "with-time", not(feature = "with-chrono")))] + "TIME" => { + use time::Time; + Value::TimeTime( + row.try_get::, _>(c.ordinal()) + .expect("Failed to get time") + .map(Box::new), + ) + } + + _ => unreachable!("Unknown column type: {}", c.type_info().name()), + }, + ) + }) + .collect(), + } +} diff --git a/sea-orm-sync/src/dynamic/entity.rs b/sea-orm-sync/src/dynamic/entity.rs new file mode 100644 index 0000000000..77b9587bd5 --- /dev/null +++ b/sea-orm-sync/src/dynamic/entity.rs @@ -0,0 +1,205 @@ +use super::{FieldType, ModelType}; +use crate::{ColumnDef, ColumnTrait, DbBackend, EntityTrait, Iterable, ModelTrait, Value}; +use sea_query::{ + ArrayType, BinOper, DynIden, Expr, ExprTrait, IntoColumnRef, IntoIden, IntoLikeExpr, + IntoTableRef, SelectStatement, SimpleExpr, TableRef, +}; +use std::sync::Arc; + +#[derive(Debug)] +pub struct Entity { + schema_name: Option>, + table_name: Arc, + columns: Vec, +} + +#[derive(Debug)] +pub struct Column { + table_name: Arc, + column_name: Arc, + column_def: ColumnDef, + value_type: ArrayType, + enum_type_name: Option>, +} + +impl Entity { + pub fn schema_name(&self) -> Option<&str> { + self.schema_name.as_deref() + } + + pub fn table_name(&self) -> &str { + &self.table_name + } + + pub fn table_ref(&self) -> TableRef { + match self.schema_name() { + Some(schema) => (schema.to_owned(), self.table_name().to_owned()).into_table_ref(), + None => self.table_name().to_owned().into_table_ref(), + } + } + + pub fn iter_columns(&self) -> impl Iterator { + self.columns.iter() + } +} + +impl Entity { + pub fn from_entity(entity: E) -> Self { + Self { + schema_name: entity.schema_name().map(Arc::from), + table_name: Arc::from(entity.table_name()), + columns: ::iter() + .map(|c| { + let (tbl, col) = c.as_column_ref(); + Column { + table_name: Arc::from(tbl.inner()), + column_name: Arc::from(col.inner()), + column_def: c.def(), + value_type: ::get_value_type(c), + enum_type_name: c.enum_type_name().map(Arc::from), + } + }) + .collect(), + } + } + + pub fn to_model_type(&self) -> ModelType { + ModelType { + fields: self + .columns + .iter() + .map(|c| FieldType { + field: c.column_name.clone(), + type_: c.value_type.clone(), + }) + .collect(), + } + } +} + +impl Column { + pub fn def(&self) -> ColumnDef { + self.column_def.clone() + } + + pub fn column_name(&self) -> &str { + &self.column_name + } + + pub fn enum_type_name(&self) -> Option<&str> { + self.enum_type_name.as_deref() + } + + pub fn entity_name(&self) -> DynIden { + self.table_name.to_string().into_iden() + } + + pub fn as_column_ref(&self) -> (DynIden, DynIden) { + ( + self.entity_name(), + self.column_name().to_owned().into_iden(), + ) + } + + crate::entity::column::macros::bind_oper!(pub eq, Equal); + crate::entity::column::macros::bind_oper!(pub ne, NotEqual); + crate::entity::column::macros::bind_oper!(pub gt, GreaterThan); + crate::entity::column::macros::bind_oper!(pub gte, GreaterThanOrEqual); + crate::entity::column::macros::bind_oper!(pub lt, SmallerThan); + crate::entity::column::macros::bind_oper!(pub lte, SmallerThanOrEqual); + + pub fn between(&self, a: V, b: V) -> SimpleExpr + where + V: Into, + { + Expr::col(self.as_column_ref()).between(a, b) + } + + pub fn not_between(&self, a: V, b: V) -> SimpleExpr + where + V: Into, + { + Expr::col(self.as_column_ref()).not_between(a, b) + } + + pub fn like(&self, s: T) -> SimpleExpr + where + T: IntoLikeExpr, + { + Expr::col(self.as_column_ref()).like(s) + } + + pub fn not_like(&self, s: T) -> SimpleExpr + where + T: IntoLikeExpr, + { + Expr::col(self.as_column_ref()).not_like(s) + } + + pub fn starts_with(&self, s: T) -> SimpleExpr + where + T: Into, + { + let pattern = format!("{}%", s.into()); + Expr::col(self.as_column_ref()).like(pattern) + } + + pub fn ends_with(&self, s: T) -> SimpleExpr + where + T: Into, + { + let pattern = format!("%{}", s.into()); + Expr::col(self.as_column_ref()).like(pattern) + } + + pub fn contains(&self, s: T) -> SimpleExpr + where + T: Into, + { + let pattern = format!("%{}%", s.into()); + Expr::col(self.as_column_ref()).like(pattern) + } + + crate::entity::column::macros::bind_func_no_params!(pub max); + crate::entity::column::macros::bind_func_no_params!(pub min); + crate::entity::column::macros::bind_func_no_params!(pub sum); + crate::entity::column::macros::bind_func_no_params!(pub count); + crate::entity::column::macros::bind_func_no_params!(pub is_null); + crate::entity::column::macros::bind_func_no_params!(pub is_not_null); + + pub fn if_null(&self, v: V) -> SimpleExpr + where + V: Into, + { + Expr::col(self.as_column_ref()).if_null(v) + } + + crate::entity::column::macros::bind_vec_func!(pub is_in); + crate::entity::column::macros::bind_vec_func!(pub is_not_in); + + crate::entity::column::macros::bind_subquery_func!(pub in_subquery); + crate::entity::column::macros::bind_subquery_func!(pub not_in_subquery); + + pub fn into_expr(self) -> Expr { + SimpleExpr::Column(self.as_column_ref().into_column_ref()) + } + + #[allow(clippy::match_single_binding)] + pub fn into_returning_expr(self, db_backend: DbBackend) -> Expr { + match db_backend { + _ => Expr::col(self.column_name().to_owned()), + } + } + + pub fn select_as(&self, expr: Expr) -> SimpleExpr { + crate::entity::column::cast_enum_as( + expr, + &self.def(), + crate::entity::column::select_enum_as, + ) + } + + pub fn save_as(&self, val: Expr) -> SimpleExpr { + crate::entity::column::cast_enum_as(val, &self.def(), crate::entity::column::save_enum_as) + } +} diff --git a/sea-orm-sync/src/dynamic/execute.rs b/sea-orm-sync/src/dynamic/execute.rs new file mode 100644 index 0000000000..86be34d8b6 --- /dev/null +++ b/sea-orm-sync/src/dynamic/execute.rs @@ -0,0 +1,105 @@ +use crate::{ + ConnectionTrait, DbBackend, DbErr, EntityTrait, FromQueryResult, QueryResult, Select, + Statement, dynamic, +}; +use itertools::Itertools; +use sea_query::{DynIden, Expr, IntoIden, SelectStatement}; +use std::marker::PhantomData; + +#[derive(Debug)] +pub struct SelectModelAndDynModel +where + M: FromQueryResult, +{ + model: PhantomData, + dyn_model: dynamic::ModelType, +} + +#[derive(Clone, Debug)] +pub struct DynSelector +where + S: DynSelectorTrait, +{ + pub(crate) query: SelectStatement, + selector: S, +} + +pub trait DynSelectorTrait { + type Item: Sized; + + #[allow(clippy::wrong_self_convention)] + fn from_raw_query_result(&self, res: QueryResult) -> Result; +} + +impl DynSelectorTrait for SelectModelAndDynModel +where + M: FromQueryResult + Sized, +{ + type Item = (M, dynamic::Model); + + fn from_raw_query_result(&self, res: QueryResult) -> Result { + Ok(( + M::from_query_result(&res, "")?, + self.dyn_model.from_query_result(&res, "")?, + )) + } +} + +impl Select +where + E: EntityTrait, +{ + pub fn select_also_dyn_model( + mut self, + table: DynIden, + dyn_model: dynamic::ModelType, + ) -> DynSelector> { + for field in dyn_model.fields.iter() { + self.query.expr(Expr::col(( + table.clone(), + field.field().to_owned().into_iden(), + ))); + } + DynSelector { + query: self.query, + selector: SelectModelAndDynModel { + model: PhantomData, + dyn_model, + }, + } + } +} + +impl DynSelector +where + S: DynSelectorTrait, +{ + /// Get the SQL statement + pub fn into_statement(self, builder: DbBackend) -> Statement { + builder.build(&self.query) + } + + /// Get an item from the Select query + pub fn one(mut self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + self.query.limit(1); + let row = db.query_one(&self.query)?; + match row { + Some(row) => Ok(Some(self.selector.from_raw_query_result(row)?)), + None => Ok(None), + } + } + + /// Get all items from the Select query + pub fn all(self, db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + db.query_all(&self.query)? + .into_iter() + .map(|row| self.selector.from_raw_query_result(row)) + .try_collect() + } +} diff --git a/sea-orm-sync/src/dynamic/mod.rs b/sea-orm-sync/src/dynamic/mod.rs new file mode 100644 index 0000000000..4126316ef8 --- /dev/null +++ b/sea-orm-sync/src/dynamic/mod.rs @@ -0,0 +1,10 @@ +//! The API of this module is not yet stable, and may have breaking changes between minor versions. +#![allow(missing_docs)] + +mod entity; +mod execute; +mod model; + +pub use entity::*; +pub use execute::*; +pub use model::*; diff --git a/sea-orm-sync/src/dynamic/model.rs b/sea-orm-sync/src/dynamic/model.rs new file mode 100644 index 0000000000..576e3a9203 --- /dev/null +++ b/sea-orm-sync/src/dynamic/model.rs @@ -0,0 +1,189 @@ +use crate::{DbErr, QueryResult}; +use sea_query::{ArrayType, DynIden, Value}; +use std::sync::Arc; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModelType { + pub fields: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FieldType { + pub(super) field: Arc, + pub(super) type_: ArrayType, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Model { + pub fields: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FieldValue { + pub(super) field: Arc, + pub value: Value, +} + +impl FieldType { + pub fn new(iden: DynIden, type_: ArrayType) -> Self { + Self { + field: Arc::from(iden.inner()), + type_, + } + } + + pub fn field(&self) -> &str { + &self.field + } +} + +impl FieldValue { + pub fn field(&self) -> &str { + &self.field + } +} + +impl ModelType { + pub fn from_query_result(&self, res: &QueryResult, pre: &str) -> Result { + let mut fields = Vec::new(); + for f in self.fields.iter() { + fields.push(FieldValue { + field: f.field.clone(), + value: try_get(res, pre, f.field(), &f.type_)?, + }); + } + Ok(Model { fields }) + } +} + +impl Model { + pub fn try_get(&self, col: &str) -> Result<&Value, DbErr> { + for field in &self.fields { + if field.field() == col { + return Ok(&field.value); + } + } + Err(DbErr::Type(format!("{col} not exist"))) + } +} + +fn try_get(res: &QueryResult, pre: &str, col: &str, ty: &ArrayType) -> Result { + // how to handle postgres-array? + Ok(match ty { + ArrayType::Bool => Value::Bool(res.try_get(pre, col)?), + ArrayType::TinyInt => Value::TinyInt(res.try_get(pre, col)?), + ArrayType::SmallInt => Value::SmallInt(res.try_get(pre, col)?), + ArrayType::Int => Value::Int(res.try_get(pre, col)?), + ArrayType::BigInt => Value::BigInt(res.try_get(pre, col)?), + ArrayType::TinyUnsigned => Value::TinyUnsigned(res.try_get(pre, col)?), + ArrayType::SmallUnsigned => Value::SmallUnsigned(res.try_get(pre, col)?), + ArrayType::Unsigned => Value::Unsigned(res.try_get(pre, col)?), + ArrayType::BigUnsigned => Value::BigUnsigned(res.try_get(pre, col)?), + ArrayType::Float => Value::Float(res.try_get(pre, col)?), + ArrayType::Double => Value::Double(res.try_get(pre, col)?), + ArrayType::String => Value::String(res.try_get(pre, col)?), + ArrayType::Char => return Err(DbErr::Type("Unsupported type: char".into())), + ArrayType::Bytes => Value::Bytes(res.try_get(pre, col)?), + + #[cfg(feature = "with-json")] + ArrayType::Json => Value::Json(res.try_get::>(pre, col)?.map(Box::new)), + + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDate => Value::ChronoDate(res.try_get(pre, col)?), + + #[cfg(feature = "with-chrono")] + ArrayType::ChronoTime => Value::ChronoTime(res.try_get(pre, col)?), + + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTime => Value::ChronoDateTime(res.try_get(pre, col)?), + + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTimeUtc => Value::ChronoDateTimeUtc(res.try_get(pre, col)?), + + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTimeLocal => Value::ChronoDateTimeLocal(res.try_get(pre, col)?), + + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDateTimeWithTimeZone => { + Value::ChronoDateTimeWithTimeZone(res.try_get(pre, col)?) + } + + #[cfg(feature = "with-time")] + ArrayType::TimeDate => Value::TimeDate(res.try_get(pre, col)?), + + #[cfg(feature = "with-time")] + ArrayType::TimeTime => Value::TimeTime(res.try_get(pre, col)?), + + #[cfg(feature = "with-time")] + ArrayType::TimeDateTime => Value::TimeDateTime(res.try_get(pre, col)?), + + #[cfg(feature = "with-time")] + ArrayType::TimeDateTimeWithTimeZone => { + Value::TimeDateTimeWithTimeZone(res.try_get(pre, col)?) + } + + #[cfg(feature = "with-uuid")] + ArrayType::Uuid => Value::Uuid(res.try_get(pre, col)?), + + #[cfg(feature = "with-rust_decimal")] + ArrayType::Decimal => Value::Decimal(res.try_get(pre, col)?), + + #[cfg(feature = "with-bigdecimal")] + ArrayType::BigDecimal => { + Value::BigDecimal(res.try_get::>(pre, col)?.map(Box::new)) + } + + #[cfg(feature = "with-ipnetwork")] + ArrayType::IpNetwork => Value::IpNetwork(res.try_get(pre, col)?), + }) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{QueryResultRow, database::IntoMockRow, dynamic::Entity}; + + #[test] + fn test_from_query_result() { + let result = QueryResult { + row: QueryResultRow::Mock( + crate::tests_cfg::cake::Model { + id: 12, + name: "hello".into(), + } + .into_mock_row(), + ), + }; + let model_ty = Entity::from_entity(crate::tests_cfg::cake::Entity).to_model_type(); + assert_eq!( + model_ty, + ModelType { + fields: vec![ + FieldType { + field: Arc::from("id"), + type_: ArrayType::Int, + }, + FieldType { + field: Arc::from("name"), + type_: ArrayType::String, + }, + ], + } + ); + assert_eq!( + model_ty.from_query_result(&result, "").unwrap(), + Model { + fields: vec![ + FieldValue { + field: Arc::from("id"), + value: 12i32.into(), + }, + FieldValue { + field: Arc::from("name"), + value: "hello".into(), + } + ], + } + ); + } +} diff --git a/sea-orm-sync/src/entity/ARROW.md b/sea-orm-sync/src/entity/ARROW.md new file mode 100644 index 0000000000..b7e0cd8ea4 --- /dev/null +++ b/sea-orm-sync/src/entity/ARROW.md @@ -0,0 +1,296 @@ +# Apache Arrow Type Support in SeaORM + +This document explains Apache Arrow's decimal and timestamp formats and their integration with SeaORM. + +## Arrow Decimal Types Overview + +Apache Arrow provides fixed-point decimal types for representing precise numeric values with a specific precision and scale: + +### 1. **Decimal64** +- **Storage**: 64-bit (8 bytes) fixed-point decimal +- **Precision**: Up to 18 decimal digits +- **Format**: `DataType::Decimal64(precision, scale)` + - `precision` (u8): Total number of decimal digits (1-18) + - `scale` (i8): Number of digits after decimal point (can be negative) +- **Use case**: Compact precision decimals that fit in an i64 (e.g., prices, exchange rates) +- **Example**: `Decimal64(10, 2)` represents numbers like `12345678.90` + +### 2. **Decimal128** +- **Storage**: 128-bit (16 bytes) fixed-point decimal +- **Precision**: Up to 38 decimal digits +- **Format**: `DataType::Decimal128(precision, scale)` + - `precision` (u8): Total number of decimal digits (1-38) + - `scale` (i8): Number of digits after decimal point (can be negative) +- **Use case**: Standard precision decimals (e.g., financial calculations requiring > 18 digits) +- **Example**: `Decimal128(20, 4)` represents numbers like `9999999999999999.9999` + +### 3. **Decimal256** +- **Storage**: 256-bit (32 bytes) fixed-point decimal +- **Precision**: Up to 76 decimal digits +- **Format**: `DataType::Decimal256(precision, scale)` + - `precision` (u8): Total number of decimal digits (1-76) + - `scale` (i8): Number of digits after decimal point (can be negative) +- **Use case**: High-precision scientific calculations, very large numbers +- **Example**: `Decimal256(76, 20)` for extreme precision requirements + +### Precision vs Scale + +- **Precision**: Total number of significant digits (before + after decimal point) +- **Scale**: Number of digits after the decimal point + - Positive scale: digits after decimal (e.g., scale=2 → `123.45`) + - Zero scale: integer (e.g., scale=0 → `12345`) + - Negative scale: multiplier (e.g., scale=-2 → `12300` stored as `123`) + +**Examples**: +``` +Decimal64(10, 2) → Can store: -99999999.99 to 99999999.99 (compact, i64) +Decimal64(5, 0) → Can store: -99999 to 99999 (integers, compact) +Decimal128(20, 4) → Can store: -9999999999999999.9999 to 9999999999999999.9999 +Decimal128(10, 4) → Can store: -999999.9999 to 999999.9999 +Decimal256(38, 10) → High precision: up to 28 digits before, 10 after decimal +``` + +## SeaORM Decimal Support + +SeaORM supports two Rust decimal libraries: + +### 1. **rust_decimal** (feature: `with-rust_decimal`) +- Type: `rust_decimal::Decimal` +- Precision: 28-29 significant digits +- Scale: 0-28 +- Storage: 128-bit +- Value type: `Value::Decimal` +- Best for: Most business applications, financial calculations + +### 2. **bigdecimal** (feature: `with-bigdecimal`) +- Type: `bigdecimal::BigDecimal` +- Precision: Arbitrary (limited by memory) +- Scale: Arbitrary +- Storage: Variable (uses BigInt internally) +- Value type: `Value::BigDecimal` +- Best for: Arbitrary precision requirements, scientific computing + +## Arrow → SeaORM Mapping + +### Decimal64Array → rust_decimal::Decimal +- **When**: Feature `with-rust_decimal` is enabled +- **Limitation**: Precision ≤ 18 (fits in i64) +- **Conversion**: Cast i64 to i128, then `Decimal::from_i128_with_scale()` +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` + +### Decimal64Array → bigdecimal::BigDecimal +- **When**: Feature `with-bigdecimal` is enabled (fallback if rust_decimal not available) +- **Conversion**: Convert i64 via BigInt +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` + +### Decimal128Array → rust_decimal::Decimal +- **When**: Feature `with-rust_decimal` is enabled +- **Limitation**: Precision ≤ 28, Scale ≤ 28 +- **Conversion**: Direct mapping using `i128` to `Decimal::from_i128_with_scale()` +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` + +### Decimal128Array → bigdecimal::BigDecimal +- **When**: Feature `with-bigdecimal` is enabled (fallback if rust_decimal fails or not available) +- **Limitation**: None (arbitrary precision) +- **Conversion**: Convert via BigInt +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` or `ColumnType::Money(_)` + +### Decimal256Array → bigdecimal::BigDecimal +- **When**: Feature `with-bigdecimal` is enabled +- **Required**: BigDecimal for precision > 38 +- **Conversion**: Convert via byte array to BigInt, then apply scale +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` + +## Implementation Strategy + +1. **Decimal64Array**: + - Try `with-rust_decimal` first (always fits, precision ≤ 18) + - Fallback to `with-bigdecimal` if needed + - Return type error if neither feature is enabled + +2. **Decimal128Array**: + - Try `with-rust_decimal` first (if precision/scale fit) + - Fallback to `with-bigdecimal` if needed + - Return type error if neither feature is enabled + +3. **Decimal256Array**: + - Requires `with-bigdecimal` (rust_decimal can't handle precision > 28) + - Convert byte representation to BigInt + - Apply scale to create BigDecimal + +4. **Null Handling**: + - Return `Value::Decimal(None)` or `Value::BigDecimal(None)` for null values + +--- + +# Apache Arrow Timestamp Types Support + +Arrow provides several temporal types for representing dates, times, and timestamps with varying precision. + +## Arrow Temporal Types Overview + +### Date Types + +#### 1. **Date32** +- **Storage**: 32-bit signed integer +- **Unit**: Days since Unix epoch (1970-01-01) +- **Format**: `DataType::Date32` +- **Range**: Approximately ±5.8 million years from epoch +- **Use case**: Calendar dates without time component + +#### 2. **Date64** +- **Storage**: 64-bit signed integer +- **Unit**: Milliseconds since Unix epoch +- **Format**: `DataType::Date64` +- **Range**: Much larger than Date32 +- **Use case**: Dates with millisecond precision (though time is typically zeroed) + +### Time Types + +#### 1. **Time32** +- **Storage**: 32-bit signed integer +- **Units**: Second or Millisecond +- **Variants**: + - `Time32(TimeUnit::Second)` - seconds since midnight + - `Time32(TimeUnit::Millisecond)` - milliseconds since midnight +- **Range**: 0 to 86,399 seconds (00:00:00 to 23:59:59) +- **Use case**: Time of day without date + +#### 2. **Time64** +- **Storage**: 64-bit signed integer +- **Units**: Microsecond or Nanosecond +- **Variants**: + - `Time64(TimeUnit::Microsecond)` - microseconds since midnight + - `Time64(TimeUnit::Nanosecond)` - nanoseconds since midnight +- **Range**: 0 to 86,399,999,999,999 nanoseconds +- **Use case**: High-precision time of day + +### Timestamp Types + +**Timestamp** types represent absolute points in time with optional timezone. + +- **Storage**: 64-bit signed integer +- **Units**: Second, Millisecond, Microsecond, or Nanosecond +- **Timezone**: Optional timezone string (e.g., "UTC", "America/New_York") +- **Format**: `Timestamp(TimeUnit, Option)` + +**Variants**: +```rust +DataType::Timestamp(TimeUnit::Second, None) // No timezone +DataType::Timestamp(TimeUnit::Millisecond, None) // No timezone +DataType::Timestamp(TimeUnit::Microsecond, None) // No timezone +DataType::Timestamp(TimeUnit::Nanosecond, None) // No timezone + +DataType::Timestamp(TimeUnit::Second, Some("UTC".into())) // With timezone +DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())) // With timezone +DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())) // With timezone +``` + +**TimeUnit Precision**: +- **Second**: 1 second precision (1,000,000,000 ns) +- **Millisecond**: 1 millisecond precision (1,000,000 ns) +- **Microsecond**: 1 microsecond precision (1,000 ns) +- **Nanosecond**: 1 nanosecond precision (highest) + +## SeaORM Temporal Type Support + +SeaORM supports two Rust datetime libraries for temporal types: + +### 1. **chrono** (feature: `with-chrono`) - Preferred +- Type Mappings: + - `chrono::NaiveDate` - Date without timezone + - `chrono::NaiveTime` - Time without date/timezone + - `chrono::NaiveDateTime` - DateTime without timezone + - `chrono::DateTime` - DateTime with UTC timezone +- Value types: + - `Value::ChronoDate(Option)` + - `Value::ChronoTime(Option)` + - `Value::ChronoDateTime(Option)` + - `Value::ChronoDateTimeUtc(Option>)` +- Best for: Most applications needing date/time support + +### 2. **time** (feature: `with-time`) - Alternative +- Type Mappings: + - `time::Date` - Date without timezone + - `time::Time` - Time without date/timezone + - `time::PrimitiveDateTime` - DateTime without timezone + - `time::OffsetDateTime` - DateTime with timezone offset +- Value types: + - `Value::TimeDate(Option)` + - `Value::TimeTime(Option