Merge branch 'master' into sea-query-v0.27

This commit is contained in:
Billy Chan 2022-10-17 17:42:10 +08:00
commit 29da37b4f2
No known key found for this signature in database
GPG Key ID: A2D690CAC7DF3CC7
305 changed files with 6907 additions and 2245 deletions

View File

@ -55,10 +55,11 @@ on:
push:
branches:
- master
- 0.2.x
- 0.*.x
- pr/**/ci
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }}
cancel-in-progress: true
env:
@ -393,6 +394,10 @@ jobs:
toolchain: stable
override: true
- name: check rustfmt
run: |
cargo fmt --manifest-path ${{ matrix.path }} --all -- --check
- uses: actions-rs/cargo@v1
with:
command: build
@ -405,11 +410,14 @@ jobs:
args: >
--manifest-path ${{ matrix.path }}
- name: check rustfmt
run: |
rustup override set nightly
rustup component add rustfmt
cargo +nightly fmt --manifest-path ${{ matrix.path }} --all -- --check
- name: Run mock test if it is core crate
uses: actions-rs/cargo@v1
if: ${{ contains(matrix.path, 'core/Cargo.toml') }}
with:
command: test
args: >
--manifest-path ${{ matrix.path }}
--features mock
issues-matrix:
name: Issues Matrix

View File

@ -7,13 +7,58 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## 0.10.0 - Pending
### New Features
* Better error types (carrying SQLx Error) https://github.com/SeaQL/sea-orm/pull/1002
* [sea-orm-cli] Generate entity files as a library or module https://github.com/SeaQL/sea-orm/pull/953
* [sea-orm-cli] Generate a new migration template with name prefix of unix timestamp https://github.com/SeaQL/sea-orm/pull/947
* [sea-orm-cli] Generate migration in modules https://github.com/SeaQL/sea-orm/pull/933
* [sea-orm-cli] Generate `DeriveRelation` on empty `Relation` enum https://github.com/SeaQL/sea-orm/pull/1019
* [sea-orm-cli] Generate entity derive `Eq` if possible https://github.com/SeaQL/sea-orm/pull/988
* Run migration on any PostgreSQL schema https://github.com/SeaQL/sea-orm/pull/1056
### Enhancements
* Support `distinct` & `distinct_on` expression https://github.com/SeaQL/sea-orm/pull/902
* `fn column()` also handle enum type https://github.com/SeaQL/sea-orm/pull/973
* Added `acquire_timeout` on `ConnectOptions` https://github.com/SeaQL/sea-orm/pull/897
* `migrate fresh` command will drop all PostgreSQL types https://github.com/SeaQL/sea-orm/pull/864, https://github.com/SeaQL/sea-orm/pull/991
* Better compile error for entity without primary key https://github.com/SeaQL/sea-orm/pull/1020
* Added blanket implementations of `IntoActiveValue` for `Option` values https://github.com/SeaQL/sea-orm/pull/833
* Added `into_model` & `into_json` to `Cursor` https://github.com/SeaQL/sea-orm/pull/1112
* Added `set_schema_search_path` method to `ConnectOptions` for setting schema search path of PostgreSQL connection https://github.com/SeaQL/sea-orm/pull/1056
### Bug fixes
* Trim spaces when paginating raw SQL https://github.com/SeaQL/sea-orm/pull/1094
### Breaking changes
* Replaced `usize` with `u64` in `PaginatorTrait` https://github.com/SeaQL/sea-orm/pull/789
* Type signature of `DbErr` changed as a result of https://github.com/SeaQL/sea-orm/pull/1002
### House keeping
* Documentation grammar fixes https://github.com/SeaQL/sea-orm/pull/1050
* Replace `dotenv` with `dotenvy` in examples https://github.com/SeaQL/sea-orm/pull/1085
* Exclude test_cfg module from SeaORM https://github.com/SeaQL/sea-orm/pull/1077
### Integration
* Support `rocket_okapi` https://github.com/SeaQL/sea-orm/pull/1071
## 0.9.3 - 2022-09-30
### Enhancements
* `fn column()` also handle enum type https://github.com/SeaQL/sea-orm/pull/973
* Generate migration in modules https://github.com/SeaQL/sea-orm/pull/933
* Generate `DeriveRelation` on empty `Relation` enum https://github.com/SeaQL/sea-orm/pull/1019
* Documentation grammar fixes https://github.com/SeaQL/sea-orm/pull/1050
### Bug fixes
* Implement `IntoActiveValue` for `time` types https://github.com/SeaQL/sea-orm/pull/1041
* Fixed module import for `FromJsonQueryResult` derive macro https://github.com/SeaQL/sea-orm/pull/1081
## 0.9.2 - 2022-08-20
@ -93,7 +138,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
* `SelectTwoMany::one()` has been dropped https://github.com/SeaQL/sea-orm/pull/813, you can get `(Entity, Vec<RelatedEntity>)` by first querying a single model from Entity, then use [`ModelTrait::find_related`] on the model.
* #### Feature flag revamp
We now adopt the [weak dependency](https://blog.rust-lang.org/2022/04/07/Rust-1.60.0.html#new-syntax-for-cargo-features) syntax in Cargo. That means the flags `["sqlx-json", "sqlx-chrono", "sqlx-decimal", "sqlx-uuid", "sqlx-time"]` are not needed and now removed. Instead, `with-time` will enable `sqlx?/time` only if `sqlx` is already enabled. As a consequence, now the features `with-json`, `with-chrono`, `with-rust_decimal`, `with-uuid`, `with-time` will not be enabled as a side-effects of enabling `sqlx`.
We now adopt the [weak dependency](https://blog.rust-lang.org/2022/04/07/Rust-1.60.0.html#new-syntax-for-cargo-features) syntax in Cargo. That means the flags `["sqlx-json", "sqlx-chrono", "sqlx-decimal", "sqlx-uuid", "sqlx-time"]` are not needed and now removed. Instead, `with-time` will enable `sqlx?/time` only if `sqlx` is already enabled. As a consequence, now the features `with-json`, `with-chrono`, `with-rust_decimal`, `with-uuid`, `with-time` will not be enabled as a side-effect of enabling `sqlx`.
## sea-orm-migration 0.8.3
@ -186,9 +231,9 @@ In this minor release, we removed `time` v0.1 from the dependency graph
### Fixed Issues
* orm-cli generated incorrect type for #[sea_orm(primary_key)]. Should be u64. Was i64. https://github.com/SeaQL/sea-orm/issues/295
* how to update dynamicly from json value https://github.com/SeaQL/sea-orm/issues/346
* how to update dynamically from json value https://github.com/SeaQL/sea-orm/issues/346
* Make `DatabaseConnection` `Clone` with the default features enabled https://github.com/SeaQL/sea-orm/issues/438
* Updating mutiple fields in a Model by passing a reference https://github.com/SeaQL/sea-orm/issues/460
* Updating multiple fields in a Model by passing a reference https://github.com/SeaQL/sea-orm/issues/460
* SeaORM CLI not adding serde derives to Enums https://github.com/SeaQL/sea-orm/issues/461
* sea-orm-cli generates wrong data type for nullable blob https://github.com/SeaQL/sea-orm/issues/490
* Support the time crate in addition (instead of?) chrono https://github.com/SeaQL/sea-orm/issues/499
@ -198,7 +243,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
* Datetime fields are not serialized by `.into_json()` on queries https://github.com/SeaQL/sea-orm/issues/530
* Update / Delete by id https://github.com/SeaQL/sea-orm/issues/552
* `#[sea_orm(indexed)]` only works for MySQL https://github.com/SeaQL/sea-orm/issues/554
* `sea-orm-cli generate --with-serde` does not work on Postegresql custom type https://github.com/SeaQL/sea-orm/issues/581
* `sea-orm-cli generate --with-serde` does not work on Postgresql custom type https://github.com/SeaQL/sea-orm/issues/581
* `sea-orm-cli generate --expanded-format` panic when postgres table contains enum type https://github.com/SeaQL/sea-orm/issues/614
* UUID fields are not serialized by `.into_json()` on queries https://github.com/SeaQL/sea-orm/issues/619
@ -241,7 +286,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
## 0.5.0 - 2022-01-01
### Fixed Issues
* Why insert, update, etc return a ActiveModel instead of Model? https://github.com/SeaQL/sea-orm/issues/289
* Why insert, update, etc return an ActiveModel instead of Model? https://github.com/SeaQL/sea-orm/issues/289
* Rework `ActiveValue` https://github.com/SeaQL/sea-orm/issues/321
* Some missing ActiveEnum utilities https://github.com/SeaQL/sea-orm/issues/338
@ -257,7 +302,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
* Add docker create script for contributors to setup databases locally by @billy1624 in https://github.com/SeaQL/sea-orm/pull/378
* Log with tracing-subscriber by @billy1624 in https://github.com/SeaQL/sea-orm/pull/399
* Codegen SQLite by @billy1624 in https://github.com/SeaQL/sea-orm/pull/386
* PR without clippy warmings in file changed tab by @billy1624 in https://github.com/SeaQL/sea-orm/pull/401
* PR without clippy warnings in file changed tab by @billy1624 in https://github.com/SeaQL/sea-orm/pull/401
* Rename `sea-strum` lib back to `strum` by @billy1624 in https://github.com/SeaQL/sea-orm/pull/361
### Breaking Changes
@ -275,7 +320,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
### Fixed Issues
* Delete::many() doesn't work when schema_name is defined https://github.com/SeaQL/sea-orm/issues/362
* find_with_related panic https://github.com/SeaQL/sea-orm/issues/374
* How to define rust type of TIMESTAMP? https://github.com/SeaQL/sea-orm/issues/344
* How to define the rust type of TIMESTAMP? https://github.com/SeaQL/sea-orm/issues/344
* Add Table on the generated Column enum https://github.com/SeaQL/sea-orm/issues/356
### Merged PRs
@ -317,7 +362,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
* Codegen fix clippy warnings by @billy1624 in https://github.com/SeaQL/sea-orm/pull/303
* Add axum example by @YoshieraHuang in https://github.com/SeaQL/sea-orm/pull/297
* Enumeration by @billy1624 in https://github.com/SeaQL/sea-orm/pull/258
* Add `PaginatorTrait` and `CountTrait` for more constrains by @YoshieraHuang in https://github.com/SeaQL/sea-orm/pull/306
* Add `PaginatorTrait` and `CountTrait` for more constraints by @YoshieraHuang in https://github.com/SeaQL/sea-orm/pull/306
* Continue `PaginatorTrait` by @billy1624 in https://github.com/SeaQL/sea-orm/pull/307
* Refactor `Schema` by @billy1624 in https://github.com/SeaQL/sea-orm/pull/309
* Detailed connection errors by @billy1624 in https://github.com/SeaQL/sea-orm/pull/312
@ -328,7 +373,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
* Returning by @billy1624 in https://github.com/SeaQL/sea-orm/pull/292
### Breaking Changes
* Refactor `paginate()` & `count()` utilities into `PaginatorTrait`. You can use the paginator as usual but you might need to import `PaginatorTrait` manually when upgrading from previous version.
* Refactor `paginate()` & `count()` utilities into `PaginatorTrait`. You can use the paginator as usual but you might need to import `PaginatorTrait` manually when upgrading from the previous version.
```rust
use futures::TryStreamExt;
use sea_orm::{entity::*, query::*, tests_cfg::cake};
@ -342,7 +387,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
// Do something on cakes: Vec<cake::Model>
}
```
* The helper struct `Schema` converting `EntityTrait` into different `sea-query` statement now has to be initialized with `DbBackend`.
* The helper struct `Schema` converting `EntityTrait` into different `sea-query` statements now has to be initialized with `DbBackend`.
```rust
use sea_orm::{tests_cfg::*, DbBackend, Schema};
use sea_orm::sea_query::TableCreateStatement;
@ -414,7 +459,7 @@ In this minor release, we removed `time` v0.1 from the dependency graph
(We are changing our Changelog format from now on)
### Fixed Issues
* Align case trasforms across derive macros https://github.com/SeaQL/sea-orm/issues/262
* Align case transforms across derive macros https://github.com/SeaQL/sea-orm/issues/262
* Added `is_null` and `is_not_null` to `ColumnTrait` https://github.com/SeaQL/sea-orm/issues/267
(The following is generated by GitHub)
@ -512,7 +557,7 @@ https://www.sea-ql.org/SeaORM/blog/2021-10-01-whats-new-in-0.2.4
- [[#191]] [sea-orm-cli] Unique key handling
- [[#182]] `find_linked` join with alias
- [[#202]] Accept both `postgres://` and `postgresql://`
- [[#208]] Support feteching T, (T, U), (T, U, P) etc
- [[#208]] Support fetching T, (T, U), (T, U, P) etc
- [[#209]] Rename column name & column enum variant
- [[#207]] Support `chrono::NaiveDate` & `chrono::NaiveTime`
- Support `Condition::not` (from sea-query)
@ -528,7 +573,7 @@ https://www.sea-ql.org/SeaORM/blog/2021-10-01-whats-new-in-0.2.4
## 0.2.3 - 2021-09-22
- [[#152]] DatabaseConnection impl `Clone`
- [[#175]] Impl `TryGetableMany` for diffrent types of generics
- [[#175]] Impl `TryGetableMany` for different types of generics
- Codegen `TimestampWithTimeZone` fixup
[#152]: https://github.com/SeaQL/sea-orm/issues/152

View File

@ -7,58 +7,60 @@ 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
- [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
- [Sensei](https://l2.technology/sensei) ([repository](https://github.com/L2-Technology/sensei)) ![GitHub stars](https://img.shields.io/github/stars/L2-Technology/sensei.svg?style=social) | A Bitcoin lightning node implementation
- [Spyglass](https://docs.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.
- [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
- [Sensei](https://l2.technology/sensei) ([repository](https://github.com/L2-Technology/sensei)) ![GitHub stars](https://img.shields.io/github/stars/L2-Technology/sensei.svg?style=social) | A Bitcoin lightning node implementation | DB: MySQL, Postgres, SQLite
- [Spyglass](https://docs.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
- [My Data My Consent](https://mydatamyconsent.com/) | Online data sharing for people and businesses simplified
- [CodeCTRL](https://codectrl.authentura.com) ([repository](https://github.com/Authentura/codectrl)) | A self-hostable code logging platform
- [CodeCTRL](https://codectrl.authentura.com) ([repository](https://github.com/Authentura/codectrl)) | A self-hostable code logging platform | DB: SQLite
### Frameworks
- [awto](https://github.com/awto-rs/awto) | Awtomate your 🦀 microservices with awto
- [tardis](https://github.com/ideal-world/tardis) | Elegant, Clean Rust development framework🛸
- [Rust Async-GraphQL Example: Caster API](https://github.com/bkonkle/rust-example-caster-api) | A demo GraphQL API using Tokio, Warp, async-graphql, and SeaOrm
- [Quasar](https://github.com/Technik97/Quasar) | Rust REST API using Actix-Web, SeaOrm and Postgres with Svelte Typescript Frontend
- [actix-react-starter-template](https://github.com/aslamplr/actix-react-starter-template) | Actix web + SeaORM + React + Redux + Redux Saga project starter template
- [awto](https://github.com/awto-rs/awto) | Awtomate your 🦀 microservices with awto | DB: Postgres
- [tardis](https://github.com/ideal-world/tardis) | Elegant, Clean Rust development framework🛸 | DB: MySQL, Postgres, SQLite
- [Rust Async-GraphQL Example: Caster API](https://github.com/bkonkle/rust-example-caster-api) | A demo GraphQL API using Tokio, Warp, async-graphql, and SeaOrm | DB: Postgres
- [Quasar](https://github.com/Technik97/Quasar) | Rust REST API using Actix-Web, SeaOrm and Postgres with Svelte Typescript Frontend | DB: Postgres
- [actix-react-starter-template](https://github.com/aslamplr/actix-react-starter-template) | Actix web + SeaORM + React + Redux + Redux Saga project starter template | DB: Postgres
- [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.
### Open Source Projects
- [Wikijump](https://github.com/scpwiki/wikijump) ([repository](https://github.com/scpwiki/wikijump/tree/develop/deepwell)) | API service for Wikijump, a fork of Wikidot
- [aeroFans](https://github.com/naryand/aerofans) | Full stack forum-like social media platform in Rust and WebAssembly
- [thrpg](https://github.com/thrpg/thrpg) | Touhou Project's secondary creative games
- [Adta](https://github.com/aaronleopold/adta) | Adta is **A**nother **D**amn **T**odo **A**pp, fun little side project
- [Axum-Book-Management](https://github.com/lz1998/axum-book-management) | CRUD system of book-management with ORM and JWT for educational purposes
- [mediarepo](https://mediarepo.trivernis.dev) ([repository](https://github.com/Trivernis/mediarepo)) | A tag-based media management application
- [THUBurrow](https://thuburrow.com) ([repository](https://github.com/BobAnkh/THUBurrow)) | A campus forum built by Next.js and Rocket
- [Backpack](https://github.com/JSH32/Backpack) | Open source self hosted file sharing platform on crack
- [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
- [aeroFans](https://github.com/naryand/aerofans) | Full stack forum-like social media platform in Rust and WebAssembly | DB: Postgres
- [thrpg](https://github.com/thrpg/thrpg) | Touhou Project's secondary creative games | 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
- [mediarepo](https://mediarepo.trivernis.dev) ([repository](https://github.com/Trivernis/mediarepo)) | A tag-based media management application | DB: SQLite
- [THUBurrow](https://thuburrow.com) ([repository](https://github.com/BobAnkh/THUBurrow)) | A campus forum built by Next.js and Rocket | DB: Postgres
- [Backpack](https://github.com/JSH32/Backpack) | Open source self hosted file sharing platform on crack | DB: MySQL, Postgres, SQLite
- [Stump](https://github.com/aaronleopold/stump) ![GitHub stars](https://img.shields.io/github/stars/aaronleopold/stump.svg?style=social) | A free and open source comics server with OPDS support
- [mugen](https://github.com/koopa1338/mugen-dms) | DMS written in 🦀
- [JinShu](https://github.com/gengteng/jinshu) | A cross-platform **I**nstant **M**essaging system written in 🦀
- [rust-juniper-playground](https://github.com/Yama-Tomo/rust-juniper-playground) | juniper with SeaORM example
- [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)
- [pansy](https://github.com/niuhuan/pansy) | An illustrations app using SeaORM, SQLite, flutter. runs on the desktop and mobile terminals
- [Orca](https://github.com/workfoxes/orca) | An No-code Test Automation platfrom using actix, SeaORM, react. runs on the desktop and cloud
- [symbols](https://github.com/nappa85/symbols) | A proc-macro utility to populates enum variants with primary keys values
- [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
- [suzuya](https://github.com/SH11235/suzuya) | A merchandise management application using SeaORM, Actix-Web, Tera
- [snmp-sim-rust](https://github.com/sonalake/snmp-sim-rust) | SNMP Simulator
- [template_flow](https://github.com/hilary888/template_flow) | An experiment exploring replacing placeholders in pre-prepared templates with their actual values
- [poem_admin](https://github.com/lingdu1234/poem_admin) | An admin panel built with poem, **Sea-orm** and Vue 3.
- [VeryRezsi](https://github.com/szattila98/veryrezsi) | VeryRezsi is a subscription and expense calculator web-application. Powered by SvelteKit on client side, and Rust on server side.
- [todo-rs](https://github.com/anshulxyz/todo-rs/) | A TUI ToDo-app written in Rust using Cursive library and SeaORM for SQLite
- [KrakenPics](https://github.com/kraken-pics/backend) | A public file host written in rust using seaorm & actix_web
- [service_auth](https://github.com/shorii/service_auth) | A simple JWT authentication web-application
- [rj45less-server](https://github.com/pmnxis/rj45less-server) | A simple unique number allocator for custom router
- [SophyCore](https://github.com/FarDragi/SophyCore) | Main system that centralizes all rules, to be used by both the discord bot and the future site
- [mugen](https://github.com/koopa1338/mugen-dms) | DMS written in 🦀 | DB: Postgres
- [JinShu](https://github.com/gengteng/jinshu) | A cross-platform **I**nstant **M**essaging system written in 🦀 | DB: MySQL, Postgres
- [rust-juniper-playground](https://github.com/Yama-Tomo/rust-juniper-playground) | juniper with SeaORM example | DB: MySQL
- [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
- [pansy](https://github.com/niuhuan/pansy) | An illustration app using SeaORM, SQLite, flutter. runs on the desktop and mobile terminals | DB: 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
- [symbols](https://github.com/nappa85/symbols) | A proc-macro utility to populates enum variants with primary keys values | DB: MySQL
- [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
- [suzuya](https://github.com/SH11235/suzuya) | A merchandise management application using SeaORM, Actix-Web, Tera | DB: Postgres
- [snmp-sim-rust](https://github.com/sonalake/snmp-sim-rust) | SNMP Simulator | DB: SQLite
- [template_flow](https://github.com/hilary888/template_flow) | An experiment exploring replacing placeholders in pre-prepared templates with their actual values | DB: Postgres
- [poem_admin](https://github.com/lingdu1234/poem_admin) | An admin panel built with poems, **Sea-orm** and Vue 3. | DB: MySQL, Postgres, SQLite
- [VeryRezsi](https://github.com/szattila98/veryrezsi) | VeryRezsi is a subscription and expense calculator web-application. Powered by SvelteKit on client side, and Rust on server side. | DB: MySQL
- [todo-rs](https://github.com/anshulxyz/todo-rs/) | A TUI ToDo-app written in Rust using Cursive library and SeaORM for SQLite | DB: SQLite
- [KrakenPics](https://github.com/kraken-pics/backend) | A public file host written in rust using seaorm & actix_web | DB: MySQL
- [service_auth](https://github.com/shorii/service_auth) | A simple JWT authentication web-application | DB: Postgres
- [rj45less-server](https://github.com/pmnxis/rj45less-server) | A simple unique number allocator for custom router | 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
- [lldap](https://github.com/nitnelave/lldap) ![GitHub stars](https://img.shields.io/github/stars/nitnelave/lldap.svg?style=social) | Light LDAP implementation for authentication
- [nitro_repo](https://github.com/wyatt-herkamp/nitro_repo) | An OpenSource, lightweight, and fast artifact manager.
- [MoonRamp](https://github.com/MoonRamp/MoonRamp) | A free and open source crypto payment gateway
- [url_shortener](https://github.com/michidk/url_shortener) | A simple self-hosted URL shortener written in Rust
- [nitro_repo](https://github.com/wyatt-herkamp/nitro_repo) | An OpenSource, lightweight, and fast artifact manager. | DB: MySQL, SQLite
- [MoonRamp](https://github.com/MoonRamp/MoonRamp) | A free and open source crypto payment gateway | DB: MySQL, Postgres, SQLite
- [url_shortener](https://github.com/michidk/url_shortener) | A simple self-hosted URL shortener written in Rust | DB: MySQL, Postgres, SQLite
- [RGB Lib](https://github.com/RGB-Tools/rgb-lib) | A library to manage wallets for RGB assets | DB: MySQL, Postgres, SQLite
- [RCloud](https://github.com/p0rtL6/RCloud) | A self-hosted lightweight cloud drive alternative
## Learning Resources
If you have article, tutorial, podcast or video reated to SeaORM and want to share it with the community, feel free to submit a PR and add it to the list below!
If you have an article, tutorial, podcast or video related to SeaORM and want to share it with the community, feel free to submit a PR and add it to the list below!
### Tutorials

View File

@ -10,11 +10,11 @@ This project is governed by the [SeaQL Code of Conduct](https://github.com/SeaQL
## I have a question
If you got a question to ask, please do not open an issue for it. It's quicker to ask us on [SeaQL Discord Server](https://discord.com/invite/uCPdDXzbdv) or open a [GitHub Discussion](https://docs.github.com/en/discussions/quickstart#creating-a-new-discussion) on the corresponding repository.
If you have a question to ask, please do not open an issue for it. It's quicker to ask us on [SeaQL Discord Server](https://discord.com/invite/uCPdDXzbdv) or open a [GitHub Discussion](https://docs.github.com/en/discussions/quickstart#creating-a-new-discussion) on the corresponding repository.
## I need a feature
Feature request from anyone is definitely welcomed! Actually, since 0.2, many features are proposed and/or contributed by non-core members, e.g. [#105](https://github.com/SeaQL/sea-orm/issues/105), [#142](https://github.com/SeaQL/sea-orm/issues/142), [#252](https://github.com/SeaQL/sea-orm/issues/252), with various degrees of involvement. We will implement feature proposals if it benefits everyone, but of course code contributions will more likely be accepted.
Feature requests from anyone is definitely welcomed! Actually, since 0.2, many features are proposed and/or contributed by non-core members, e.g. [#105](https://github.com/SeaQL/sea-orm/issues/105), [#142](https://github.com/SeaQL/sea-orm/issues/142), [#252](https://github.com/SeaQL/sea-orm/issues/252), with various degrees of involvement. We will implement feature proposals if it benefits everyone, but of course code contributions will more likely be accepted.
## I want to support
@ -22,7 +22,7 @@ Awesome! The best way to support us is to recommend it to your classmates/collea
## I want to join
We are always looking for long-term contributors. If you want to commit longer-term to SeaQL's open source effort, definitely talk with us! There may be various form of "grant" to compensate your devotion. Although at this stage we are not resourceful enough to offer a stable stream of income to contributors.
We are always looking for long-term contributors. If you want to commit longer-term to SeaQL's open source effort, definitely talk with us! There may be various forms of "grant" to compensate for your devotion. Although at this stage we are not resourceful enough to offer a stable stream of income to contributors.
## I want to sponsor
@ -51,7 +51,7 @@ Without involving a live database, you can run unit tests on your machine with t
### Integration Test
Next, if you want to run integration tests on a live database. We recommand using Docker to spawn your database instance, you can refer to [this](build-tools/docker-compose.yml) docker compose file for reference.
Next, if you want to run integration tests on a live database. We recommend using Docker to spawn your database instance, you can refer to [this](build-tools/docker-compose.yml) docker compose file for reference.
Running integration tests on a live database:
- SQLite

View File

@ -44,6 +44,7 @@ uuid = { version = "^1", features = ["serde", "v4"], optional = true }
ouroboros = "0.15"
url = "^2.2"
once_cell = "1.8"
thiserror = "^1"
[patch.crates-io]
sea-query = { git = "https://github.com/SeaQL/sea-query", branch = "sqlite-bind-decimals" }
@ -58,7 +59,7 @@ actix-rt = { version = "2.2.0" }
maplit = { version = "^1" }
rust_decimal_macros = { version = "^1" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sea-orm = { path = ".", features = ["mock", "debug-print"] }
sea-orm = { path = ".", features = ["mock", "debug-print", "tests-cfg"] }
pretty_assertions = { version = "^0.7" }
time = { version = "^0.3", features = ["macros"] }
@ -116,3 +117,4 @@ runtime-tokio-rustls = [
"sea-query-binder/runtime-tokio-rustls",
"runtime-tokio",
]
tests-cfg = []

View File

@ -16,7 +16,7 @@ Avoid macros with DSL, use derive macros where appropriate. Be friendly with IDE
## Test Time
After some bitterness we realized it is not possible to capture everything compile time. But we don't
After some bitterness we realized it is not possible to capture everything at compile time. But we don't
want to encounter problems at run time either. The solution is to perform checking at 'test time' to
uncover problems. These checks will be removed at production so there will be no run time penalty.

View File

@ -26,6 +26,7 @@ Join our Discord server to chat with others in the SeaQL community!
+ [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)
Integration examples
@ -188,7 +189,7 @@ fruit::Entity::delete_many()
## Learn More
1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.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)
@ -227,7 +228,7 @@ 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 build for Rust's future.
SeaORM is a community driven project. We welcome you to participate, contribute and together help build Rust's future.
A big shout out to our contributors:

View File

@ -6,30 +6,7 @@ edition = "2021"
publish = false
[workspace]
members = [".", "entity", "migration"]
members = [".", "api", "core", "entity", "migration"]
[dependencies]
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"
dotenv = "0.15"
listenfd = "0.3.3"
serde = "1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
entity = { path = "entity" }
migration = { path = "migration" }
[dependencies.sea-orm]
path = "../../" # remove this line in your own project
version = "^0.10.0" # sea-orm version
features = [
"debug-print",
"runtime-async-std-native-tls",
"sqlx-mysql",
# "sqlx-postgres",
# "sqlx-sqlite",
]
actix3-example-api = { path = "api" }

View File

@ -6,7 +6,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 `Cargo.toml` (the `"sqlx-mysql",` line)
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
@ -18,3 +18,10 @@ Run server with auto-reloading:
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
```

View File

@ -0,0 +1,22 @@
[package]
name = "actix3-example-api"
version = "0.1.0"
authors = ["Sam Samai <sam@studio2pi.com.au>"]
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", features = ["env-filter"] }
entity = { path = "../entity" }
migration = { path = "../migration" }

View File

@ -0,0 +1,219 @@
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<u64>,
posts_per_page: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
#[get("/")]
async fn list(
req: HttpRequest,
data: web::Data<AppState>,
opt_flash: Option<actix_flash::Message<FlashData>>,
) -> Result<HttpResponse, Error> {
let template = &data.templates;
let conn = &data.conn;
// get params
let params = web::Query::<Params>::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<AppState>) -> Result<HttpResponse, Error> {
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<AppState>,
post_form: web::Form<post::Model>,
) -> actix_flash::Response<HttpResponse, FlashData> {
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<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
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<AppState>,
id: web::Path<i32>,
post_form: web::Form<post::Model>,
) -> actix_flash::Response<HttpResponse, FlashData> {
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<AppState>,
id: web::Path<i32>,
) -> actix_flash::Response<HttpResponse, FlashData> {
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)
}
}

View File

@ -0,0 +1,30 @@
[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.10.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"]

View File

@ -0,0 +1,7 @@
mod mutation;
mod query;
pub use mutation::*;
pub use query::*;
pub use sea_orm;

View File

@ -0,0 +1,53 @@
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, DbErr> {
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<post::Model, DbErr> {
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<DeleteResult, DbErr> {
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<DeleteResult, DbErr> {
Post::delete_many().exec(db).await
}
}

View File

@ -0,0 +1,26 @@
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<Option<post::Model>, 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<post::Model>, 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))
}
}

View File

@ -0,0 +1,79 @@
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);
}
}

View File

@ -0,0 +1,50 @@
use ::entity::post;
use sea_orm::*;
#[cfg(feature = "mock")]
pub fn prepare_mock_db() -> DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results(vec![
vec![post::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![post::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
vec![post::Model {
id: 6,
title: "Title D".to_owned(),
text: "Text D".to_owned(),
}],
vec![post::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![post::Model {
id: 1,
title: "New Title A".to_owned(),
text: "New Text A".to_owned(),
}],
vec![post::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
])
.append_exec_results(vec![
MockExecResult {
last_insert_id: 6,
rows_affected: 1,
},
MockExecResult {
last_insert_id: 6,
rows_affected: 5,
},
])
.into_connection()
}

View File

@ -1,227 +1,3 @@
use actix_files as fs;
use actix_web::{
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
};
use entity::post;
use entity::post::Entity as Post;
use listenfd::ListenFd;
use migration::{Migrator, MigratorTrait};
use sea_orm::DatabaseConnection;
use sea_orm::{entity::*, query::*};
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<u64>,
posts_per_page: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
#[get("/")]
async fn list(
req: HttpRequest,
data: web::Data<AppState>,
opt_flash: Option<actix_flash::Message<FlashData>>,
) -> Result<HttpResponse, Error> {
let template = &data.templates;
let conn = &data.conn;
// get params
let params = web::Query::<Params>::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 paginator = Post::find()
.order_by_asc(post::Column::Id)
.paginate(conn, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap();
let posts = paginator
.fetch_page(page - 1)
.await
.expect("could not retrieve posts");
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<AppState>) -> Result<HttpResponse, Error> {
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<AppState>,
post_form: web::Form<post::Model>,
) -> actix_flash::Response<HttpResponse, FlashData> {
let conn = &data.conn;
let form = post_form.into_inner();
post::ActiveModel {
title: Set(form.title.to_owned()),
text: Set(form.text.to_owned()),
..Default::default()
}
.save(conn)
.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<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let template = &data.templates;
let post: post::Model = Post::find_by_id(id.into_inner())
.one(conn)
.await
.expect("could not find post")
.unwrap();
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<AppState>,
id: web::Path<i32>,
post_form: web::Form<post::Model>,
) -> actix_flash::Response<HttpResponse, FlashData> {
let conn = &data.conn;
let form = post_form.into_inner();
post::ActiveModel {
id: Set(id.into_inner()),
title: Set(form.title.to_owned()),
text: Set(form.text.to_owned()),
}
.save(conn)
.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<AppState>,
id: web::Path<i32>,
) -> actix_flash::Response<HttpResponse, FlashData> {
let conn = &data.conn;
let post: post::ActiveModel = Post::find_by_id(id.into_inner())
.one(conn)
.await
.unwrap()
.unwrap()
.into();
post.delete(conn).await.unwrap();
let flash = FlashData {
kind: "success".to_owned(),
message: "Post successfully deleted.".to_owned(),
};
actix_flash::Response::with_redirect(flash, "/")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "debug");
tracing_subscriber::fmt::init();
// get env vars
dotenv::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 = sea_orm::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", "./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(())
}
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(list);
cfg.service(new);
cfg.service(create);
cfg.service(edit);
cfg.service(update);
cfg.service(delete);
fn main() {
actix3_example_api::main();
}

View File

@ -6,30 +6,7 @@ edition = "2021"
publish = false
[workspace]
members = [".", "entity", "migration"]
members = [".", "api", "core", "entity", "migration"]
[dependencies]
actix-files = "0.6"
actix-http = "3"
actix-rt = "2.7"
actix-service = "2"
actix-web = "4"
tera = "1.15.0"
dotenv = "0.15"
listenfd = "0.5"
serde = "1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
entity = { path = "entity" }
migration = { path = "migration" }
[dependencies.sea-orm]
path = "../../" # remove this line in your own project
version = "^0.10.0" # sea-orm version
features = [
"debug-print",
"runtime-actix-native-tls",
"sqlx-mysql",
# "sqlx-postgres",
# "sqlx-sqlite",
]
actix-example-api = { path = "api" }

View File

@ -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 `Cargo.toml` (the `"sqlx-mysql",` line)
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
@ -16,3 +16,10 @@ Run server with auto-reloading:
cargo install systemfd cargo-watch
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
```

View File

@ -0,0 +1,21 @@
[package]
name = "actix-example-api"
version = "0.1.0"
authors = ["Sam Samai <sam@studio2pi.com.au>"]
edition = "2021"
publish = false
[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"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
entity = { path = "../entity" }
migration = { path = "../migration" }

View File

@ -0,0 +1,215 @@
use actix_example_core::{
sea_orm::{Database, DatabaseConnection},
Mutation, Query,
};
use actix_files::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<u64>,
posts_per_page: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
#[get("/")]
async fn list(req: HttpRequest, data: web::Data<AppState>) -> Result<HttpResponse, Error> {
let template = &data.templates;
let conn = &data.conn;
// get params
let params = web::Query::<Params>::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);
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<AppState>) -> Result<HttpResponse, Error> {
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<AppState>,
post_form: web::Form<post::Model>,
) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let form = post_form.into_inner();
Mutation::create_post(conn, form)
.await
.expect("could not insert post");
Ok(HttpResponse::Found()
.append_header(("location", "/"))
.finish())
}
#[get("/{id}")]
async fn edit(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
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<AppState>,
id: web::Path<i32>,
post_form: web::Form<post::Model>,
) -> Result<HttpResponse, Error> {
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");
Ok(HttpResponse::Found()
.append_header(("location", "/"))
.finish())
}
#[post("/delete/{id}")]
async fn delete(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let id = id.into_inner();
Mutation::delete_post(conn, id)
.await
.expect("could not delete post");
Ok(HttpResponse::Found()
.append_header(("location", "/"))
.finish())
}
async fn not_found(data: web::Data<AppState>, request: HttpRequest) -> Result<HttpResponse, Error> {
let mut ctx = tera::Context::new();
ctx.insert("uri", request.uri().path());
let template = &data.templates;
let body = template
.render("error/404.html.tera", &ctx)
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
#[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);
// establish connection to database and apply migrations
// -> create post table if not exists
let conn = Database::connect(&db_url).await.unwrap();
Migrator::up(&conn, None).await.unwrap();
// load tera templates and build app state
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
let state = AppState { templates, conn };
// create server and try to serve over socket if possible
let mut listenfd = ListenFd::from_env();
let mut server = HttpServer::new(move || {
App::new()
.service(Fs::new("/static", "./api/static"))
.app_data(web::Data::new(state.clone()))
.wrap(middleware::Logger::default()) // enable logger
.default_service(web::route().to(not_found))
.configure(init)
});
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);
}
}

View File

@ -0,0 +1,30 @@
[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.10.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"]

View File

@ -0,0 +1,7 @@
mod mutation;
mod query;
pub use mutation::*;
pub use query::*;
pub use sea_orm;

View File

@ -0,0 +1,53 @@
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, DbErr> {
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<post::Model, DbErr> {
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<DeleteResult, DbErr> {
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<DeleteResult, DbErr> {
Post::delete_many().exec(db).await
}
}

View File

@ -0,0 +1,26 @@
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<Option<post::Model>, 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<post::Model>, 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))
}
}

View File

@ -0,0 +1,79 @@
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);
}
}

View File

@ -0,0 +1,50 @@
use ::entity::post;
use sea_orm::*;
#[cfg(feature = "mock")]
pub fn prepare_mock_db() -> DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results(vec![
vec![post::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![post::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
vec![post::Model {
id: 6,
title: "Title D".to_owned(),
text: "Text D".to_owned(),
}],
vec![post::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![post::Model {
id: 1,
title: "New Title A".to_owned(),
text: "New Text A".to_owned(),
}],
vec![post::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
])
.append_exec_results(vec![
MockExecResult {
last_insert_id: 6,
rows_affected: 1,
},
MockExecResult {
last_insert_id: 6,
rows_affected: 5,
},
])
.into_connection()
}

View File

@ -1,223 +1,3 @@
use actix_files::Files as Fs;
use actix_web::{
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
};
use entity::post;
use entity::post::Entity as Post;
use listenfd::ListenFd;
use migration::{Migrator, MigratorTrait};
use sea_orm::DatabaseConnection;
use sea_orm::{entity::*, query::*};
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<u64>,
posts_per_page: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
#[get("/")]
async fn list(req: HttpRequest, data: web::Data<AppState>) -> Result<HttpResponse, Error> {
let template = &data.templates;
let conn = &data.conn;
// get params
let params = web::Query::<Params>::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 paginator = Post::find()
.order_by_asc(post::Column::Id)
.paginate(conn, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap();
let posts = paginator
.fetch_page(page - 1)
.await
.expect("could not retrieve posts");
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);
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<AppState>) -> Result<HttpResponse, Error> {
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<AppState>,
post_form: web::Form<post::Model>,
) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let form = post_form.into_inner();
post::ActiveModel {
title: Set(form.title.to_owned()),
text: Set(form.text.to_owned()),
..Default::default()
}
.save(conn)
.await
.expect("could not insert post");
Ok(HttpResponse::Found()
.append_header(("location", "/"))
.finish())
}
#[get("/{id}")]
async fn edit(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let template = &data.templates;
let post: post::Model = Post::find_by_id(id.into_inner())
.one(conn)
.await
.expect("could not find post")
.unwrap();
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<AppState>,
id: web::Path<i32>,
post_form: web::Form<post::Model>,
) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let form = post_form.into_inner();
post::ActiveModel {
id: Set(id.into_inner()),
title: Set(form.title.to_owned()),
text: Set(form.text.to_owned()),
}
.save(conn)
.await
.expect("could not edit post");
Ok(HttpResponse::Found()
.append_header(("location", "/"))
.finish())
}
#[post("/delete/{id}")]
async fn delete(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
let conn = &data.conn;
let post: post::ActiveModel = Post::find_by_id(id.into_inner())
.one(conn)
.await
.unwrap()
.unwrap()
.into();
post.delete(conn).await.unwrap();
Ok(HttpResponse::Found()
.append_header(("location", "/"))
.finish())
}
async fn not_found(data: web::Data<AppState>, request: HttpRequest) -> Result<HttpResponse, Error> {
let mut ctx = tera::Context::new();
ctx.insert("uri", request.uri().path());
let template = &data.templates;
let body = template
.render("error/404.html.tera", &ctx)
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "debug");
tracing_subscriber::fmt::init();
// get env vars
dotenv::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);
// establish connection to database and apply migrations
// -> create post table if not exists
let conn = sea_orm::Database::connect(&db_url).await.unwrap();
Migrator::up(&conn, None).await.unwrap();
// load tera templates and build app state
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
let state = AppState { templates, conn };
// create server and try to serve over socket if possible
let mut listenfd = ListenFd::from_env();
let mut server = HttpServer::new(move || {
App::new()
.service(Fs::new("/static", "./static"))
.app_data(web::Data::new(state.clone()))
.wrap(middleware::Logger::default()) // enable logger
.default_service(web::route().to(not_found))
.configure(init)
});
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(())
}
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(list);
cfg.service(new);
cfg.service(create);
cfg.service(edit);
cfg.service(update);
cfg.service(delete);
fn main() {
actix_example_api::main();
}

View File

@ -5,32 +5,8 @@ authors = ["Yoshiera Huang <huangjasper@126.com>"]
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = [".", "entity", "migration"]
members = [".", "api", "core", "entity", "migration"]
[dependencies]
tokio = { version = "1.18.1", features = ["full"] }
axum = "0.5.4"
tower = "0.4.12"
tower-http = { version = "0.3.3", features = ["fs"] }
tower-cookies = "0.6.0"
anyhow = "1.0.57"
dotenv = "0.15.0"
serde = "1.0.137"
serde_json = "1.0.81"
tera = "1.15.0"
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
entity = { path = "entity" }
migration = { path = "migration" }
[dependencies.sea-orm]
path = "../../" # remove this line in your own project
version = "^0.10.0" # sea-orm version
features = [
"debug-print",
"runtime-tokio-native-tls",
"sqlx-postgres",
# "sqlx-mysql",
# "sqlx-sqlite",
]
axum-example-api = { path = "api" }

View File

@ -4,8 +4,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 `Cargo.toml` (the `"sqlx-postgres",` line)
1. Turn on the appropriate database feature for your chosen db in `core/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:
```bash
cd core
cargo test --features mock
```

View File

@ -0,0 +1,22 @@
[package]
name = "axum-example-api"
version = "0.1.0"
authors = ["Yoshiera Huang <huangjasper@126.com>"]
edition = "2021"
publish = false
[dependencies]
axum-example-core = { path = "../core" }
tokio = { version = "1.18.1", features = ["full"] }
axum = "0.5.4"
tower = "0.4.12"
tower-http = { version = "0.3.3", features = ["fs"] }
tower-cookies = "0.6.0"
anyhow = "1.0.57"
dotenvy = "0.15.0"
serde = "1.0.137"
serde_json = "1.0.81"
tera = "1.15.0"
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
entity = { path = "../entity" }
migration = { path = "../migration" }

View File

@ -0,0 +1,210 @@
mod flash;
use axum::{
extract::{Extension, Form, Path, Query},
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 migration::{Migrator, MigratorTrait};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::{env, net::SocketAddr};
use tera::Tera;
use tower::ServiceBuilder;
use tower_cookies::{CookieManagerLayer, Cookies};
use tower_http::services::ServeDir;
#[tokio::main]
async fn start() -> anyhow::Result<()> {
env::set_var("RUST_LOG", "debug");
tracing_subscriber::fmt::init();
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);
let conn = Database::connect(db_url)
.await
.expect("Database connection failed");
Migrator::up(&conn, None).await.unwrap();
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"))
.expect("Tera initialization failed");
// let state = AppState { templates, conn };
let app = Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(edit_post).post(update_post))
.route("/new", get(new_post))
.route("/delete/:id", post(delete_post))
.nest(
"/static",
get_service(ServeDir::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/static"
)))
.handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
.layer(
ServiceBuilder::new()
.layer(CookieManagerLayer::new())
.layer(Extension(conn))
.layer(Extension(templates)),
);
let addr = SocketAddr::from_str(&server_url).unwrap();
Server::bind(&addr).serve(app.into_make_service()).await?;
Ok(())
}
#[derive(Deserialize)]
struct Params {
page: Option<u64>,
posts_per_page: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
async fn list_posts(
Extension(ref templates): Extension<Tera>,
Extension(ref conn): Extension<DatabaseConnection>,
Query(params): Query<Params>,
cookies: Cookies,
) -> Result<Html<String>, (StatusCode, &'static str)> {
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(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(value) = get_flash_cookie::<FlashData>(&cookies) {
ctx.insert("flash", &value);
}
let body = templates
.render("index.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn new_post(
Extension(ref templates): Extension<Tera>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let ctx = tera::Context::new();
let body = templates
.render("new.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn create_post(
Extension(ref conn): Extension<DatabaseConnection>,
form: Form<post::Model>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
let form = form.0;
MutationCore::create_post(conn, form)
.await
.expect("could not insert post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully added".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
async fn edit_post(
Extension(ref templates): Extension<Tera>,
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let post: post::Model = QueryCore::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 = templates
.render("edit.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn update_post(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
form: Form<post::Model>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, String)> {
let form = form.0;
MutationCore::update_post_by_id(conn, id, form)
.await
.expect("could not edit post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully updated".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
async fn delete_post(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
MutationCore::delete_post(conn, id)
.await
.expect("could not delete post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully deleted".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
pub fn main() {
let result = start();
if let Some(err) = result.err() {
println!("Error: {}", err);
}
}

View File

@ -0,0 +1,30 @@
[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.10.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"]

View File

@ -0,0 +1,7 @@
mod mutation;
mod query;
pub use mutation::*;
pub use query::*;
pub use sea_orm;

View File

@ -0,0 +1,53 @@
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, DbErr> {
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<post::Model, DbErr> {
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<DeleteResult, DbErr> {
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<DeleteResult, DbErr> {
Post::delete_many().exec(db).await
}
}

View File

@ -0,0 +1,26 @@
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<Option<post::Model>, 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<post::Model>, 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))
}
}

View File

@ -0,0 +1,79 @@
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);
}
}

View File

@ -0,0 +1,50 @@
use ::entity::post;
use sea_orm::*;
#[cfg(feature = "mock")]
pub fn prepare_mock_db() -> DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results(vec![
vec![post::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![post::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
vec![post::Model {
id: 6,
title: "Title D".to_owned(),
text: "Text D".to_owned(),
}],
vec![post::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![post::Model {
id: 1,
title: "New Title A".to_owned(),
text: "New Text A".to_owned(),
}],
vec![post::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
])
.append_exec_results(vec![
MockExecResult {
last_insert_id: 6,
rows_affected: 1,
},
MockExecResult {
last_insert_id: 6,
rows_affected: 5,
},
])
.into_connection()
}

View File

@ -14,13 +14,7 @@ pub struct Model {
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,220 +1,3 @@
mod flash;
use axum::{
extract::{Extension, Form, Path, Query},
http::StatusCode,
response::Html,
routing::{get, get_service, post},
Router, Server,
};
use entity::post;
use flash::{get_flash_cookie, post_response, PostResponse};
use migration::{Migrator, MigratorTrait};
use post::Entity as Post;
use sea_orm::{prelude::*, Database, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::{env, net::SocketAddr};
use tera::Tera;
use tower::ServiceBuilder;
use tower_cookies::{CookieManagerLayer, Cookies};
use tower_http::services::ServeDir;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env::set_var("RUST_LOG", "debug");
tracing_subscriber::fmt::init();
dotenv::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);
let conn = Database::connect(db_url)
.await
.expect("Database connection failed");
Migrator::up(&conn, None).await.unwrap();
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"))
.expect("Tera initialization failed");
// let state = AppState { templates, conn };
let app = Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(edit_post).post(update_post))
.route("/new", get(new_post))
.route("/delete/:id", post(delete_post))
.nest(
"/static",
get_service(ServeDir::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/static"
)))
.handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
.layer(
ServiceBuilder::new()
.layer(CookieManagerLayer::new())
.layer(Extension(conn))
.layer(Extension(templates)),
);
let addr = SocketAddr::from_str(&server_url).unwrap();
Server::bind(&addr).serve(app.into_make_service()).await?;
Ok(())
}
#[derive(Deserialize)]
struct Params {
page: Option<u64>,
posts_per_page: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
async fn list_posts(
Extension(ref templates): Extension<Tera>,
Extension(ref conn): Extension<DatabaseConnection>,
Query(params): Query<Params>,
cookies: Cookies,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let page = params.page.unwrap_or(1);
let posts_per_page = params.posts_per_page.unwrap_or(5);
let paginator = Post::find()
.order_by_asc(post::Column::Id)
.paginate(conn, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap();
let posts = paginator
.fetch_page(page - 1)
.await
.expect("could not retrieve posts");
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(value) = get_flash_cookie::<FlashData>(&cookies) {
ctx.insert("flash", &value);
}
let body = templates
.render("index.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn new_post(
Extension(ref templates): Extension<Tera>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let ctx = tera::Context::new();
let body = templates
.render("new.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn create_post(
Extension(ref conn): Extension<DatabaseConnection>,
form: Form<post::Model>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
let model = form.0;
post::ActiveModel {
title: Set(model.title.to_owned()),
text: Set(model.text.to_owned()),
..Default::default()
}
.save(conn)
.await
.expect("could not insert post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully added".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
async fn edit_post(
Extension(ref templates): Extension<Tera>,
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let post: post::Model = Post::find_by_id(id)
.one(conn)
.await
.expect("could not find post")
.unwrap();
let mut ctx = tera::Context::new();
ctx.insert("post", &post);
let body = templates
.render("edit.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn update_post(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
form: Form<post::Model>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, String)> {
let model = form.0;
post::ActiveModel {
id: Set(id),
title: Set(model.title.to_owned()),
text: Set(model.text.to_owned()),
}
.save(conn)
.await
.expect("could not edit post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully updated".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
async fn delete_post(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
let post: post::ActiveModel = Post::find_by_id(id)
.one(conn)
.await
.unwrap()
.unwrap()
.into();
post.delete(conn).await.unwrap();
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully deleted".to_owned(),
};
Ok(post_response(&mut cookies, data))
fn main() {
axum_example_api::main();
}

View File

@ -7,22 +7,7 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = [".", "entity", "migration"]
members = [".", "api", "core", "entity", "migration"]
[dependencies]
tokio = { version = "1.0", features = ["full"] }
axum = "^0.5.1"
dotenv = "0.15.0"
async-graphql-axum = "^4.0.6"
entity = { path = "entity" }
migration = { path = "migration" }
[dependencies.sea-orm]
path = "../../" # remove this line in your own project
version = "^0.10.0" # sea-orm version
features = [
"runtime-tokio-native-tls",
# "sqlx-postgres",
# "sqlx-mysql",
"sqlx-sqlite"
]
graphql-example-api = { path = "api" }

View File

@ -6,8 +6,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 `Cargo.toml` (the `"sqlx-sqlite",` line)
1. Turn on the appropriate database feature for your chosen db in `core/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:
```bash
cd core
cargo test --features mock
```

View File

@ -0,0 +1,15 @@
[package]
name = "graphql-example-api"
authors = ["Aaron Leopold <aaronleopold1221@gmail.com>"]
version = "0.1.0"
edition = "2021"
publish = false
[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" }

View File

@ -0,0 +1,21 @@
use graphql_example_core::sea_orm::DatabaseConnection;
pub struct Database {
pub connection: DatabaseConnection,
}
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");
Database { connection }
}
pub fn get_connection(&self) -> &DatabaseConnection {
&self.connection
}
}

View File

@ -1,7 +1,7 @@
use async_graphql::{Context, Object, Result};
use entity::async_graphql::{self, InputObject, SimpleObject};
use entity::note;
use sea_orm::{ActiveModelTrait, Set};
use graphql_example_core::Mutation;
use crate::db::Database;
@ -14,6 +14,16 @@ pub struct CreateNoteInput {
pub text: String,
}
impl CreateNoteInput {
fn into_model_with_arbitrary_id(self) -> note::Model {
note::Model {
id: 0,
title: self.title,
text: self.text,
}
}
}
#[derive(SimpleObject)]
pub struct DeleteResult {
pub success: bool,
@ -31,22 +41,18 @@ impl NoteMutation {
input: CreateNoteInput,
) -> Result<note::Model> {
let db = ctx.data::<Database>().unwrap();
let conn = db.get_connection();
let note = note::ActiveModel {
title: Set(input.title),
text: Set(input.text),
..Default::default()
};
Ok(note.insert(db.get_connection()).await?)
Ok(Mutation::create_note(conn, input.into_model_with_arbitrary_id()).await?)
}
pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result<DeleteResult> {
let db = ctx.data::<Database>().unwrap();
let conn = db.get_connection();
let res = note::Entity::delete_by_id(id)
.exec(db.get_connection())
.await?;
let res = Mutation::delete_note(conn, id)
.await
.expect("Cannot delete note");
if res.rows_affected <= 1 {
Ok(DeleteResult {

View File

@ -1,6 +1,6 @@
use async_graphql::{Context, Object, Result};
use entity::{async_graphql, note};
use sea_orm::EntityTrait;
use graphql_example_core::Query;
use crate::db::Database;
@ -11,18 +11,18 @@ pub struct NoteQuery;
impl NoteQuery {
async fn get_notes(&self, ctx: &Context<'_>) -> Result<Vec<note::Model>> {
let db = ctx.data::<Database>().unwrap();
let conn = db.get_connection();
Ok(note::Entity::find()
.all(db.get_connection())
Ok(Query::get_all_notes(conn)
.await
.map_err(|e| e.to_string())?)
}
async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result<Option<note::Model>> {
let db = ctx.data::<Database>().unwrap();
let conn = db.get_connection();
Ok(note::Entity::find_by_id(id)
.one(db.get_connection())
Ok(Query::find_note_by_id(conn, id)
.await
.map_err(|e| e.to_string())?)
}

View File

@ -0,0 +1,49 @@
mod db;
mod graphql;
use entity::async_graphql;
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
response::{Html, IntoResponse},
routing::get,
Router,
};
use graphql::schema::{build_schema, AppSchema};
#[cfg(debug_assertions)]
use dotenvy::dotenv;
async fn graphql_handler(schema: Extension<AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new(
"/api/graphql",
)))
}
#[tokio::main]
pub async fn main() {
#[cfg(debug_assertions)]
dotenv().ok();
let schema = build_schema().await;
let app = Router::new()
.route(
"/api/graphql",
get(graphql_playground).post(graphql_handler),
)
.layer(Extension(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();
}

View File

@ -0,0 +1,30 @@
[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.10.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"]

View File

@ -0,0 +1,7 @@
mod mutation;
mod query;
pub use mutation::*;
pub use query::*;
pub use sea_orm;

View File

@ -0,0 +1,54 @@
use ::entity::{note, note::Entity as Note};
use sea_orm::*;
pub struct Mutation;
impl Mutation {
pub async fn create_note(db: &DbConn, form_data: note::Model) -> Result<note::Model, DbErr> {
let active_model = note::ActiveModel {
title: Set(form_data.title.to_owned()),
text: Set(form_data.text.to_owned()),
..Default::default()
};
let res = Note::insert(active_model).exec(db).await?;
Ok(note::Model {
id: res.last_insert_id,
..form_data
})
}
pub async fn update_note_by_id(
db: &DbConn,
id: i32,
form_data: note::Model,
) -> Result<note::Model, DbErr> {
let note: note::ActiveModel = Note::find_by_id(id)
.one(db)
.await?
.ok_or(DbErr::Custom("Cannot find note.".to_owned()))
.map(Into::into)?;
note::ActiveModel {
id: note.id,
title: Set(form_data.title.to_owned()),
text: Set(form_data.text.to_owned()),
}
.update(db)
.await
}
pub async fn delete_note(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
let note: note::ActiveModel = Note::find_by_id(id)
.one(db)
.await?
.ok_or(DbErr::Custom("Cannot find note.".to_owned()))
.map(Into::into)?;
note.delete(db).await
}
pub async fn delete_all_notes(db: &DbConn) -> Result<DeleteResult, DbErr> {
Note::delete_many().exec(db).await
}
}

View File

@ -0,0 +1,30 @@
use ::entity::{note, note::Entity as Note};
use sea_orm::*;
pub struct Query;
impl Query {
pub async fn find_note_by_id(db: &DbConn, id: i32) -> Result<Option<note::Model>, DbErr> {
Note::find_by_id(id).one(db).await
}
pub async fn get_all_notes(db: &DbConn) -> Result<Vec<note::Model>, DbErr> {
Note::find().all(db).await
}
/// If ok, returns (note models, num pages).
pub async fn find_notes_in_page(
db: &DbConn,
page: u64,
notes_per_page: u64,
) -> Result<(Vec<note::Model>, u64), DbErr> {
// Setup paginator
let paginator = Note::find()
.order_by_asc(note::Column::Id)
.paginate(db, notes_per_page);
let num_pages = paginator.num_pages().await?;
// Fetch paginated notes
paginator.fetch_page(page - 1).await.map(|p| (p, num_pages))
}
}

View File

@ -0,0 +1,79 @@
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);
}
}

View File

@ -0,0 +1,50 @@
use ::entity::note;
use sea_orm::*;
#[cfg(feature = "mock")]
pub fn prepare_mock_db() -> DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results(vec![
vec![note::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![note::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
vec![note::Model {
id: 6,
title: "Title D".to_owned(),
text: "Text D".to_owned(),
}],
vec![note::Model {
id: 1,
title: "Title A".to_owned(),
text: "Text A".to_owned(),
}],
vec![note::Model {
id: 1,
title: "New Title A".to_owned(),
text: "New Text A".to_owned(),
}],
vec![note::Model {
id: 5,
title: "Title C".to_owned(),
text: "Text C".to_owned(),
}],
])
.append_exec_results(vec![
MockExecResult {
last_insert_id: 6,
rows_affected: 1,
},
MockExecResult {
last_insert_id: 6,
rows_affected: 5,
},
])
.into_connection()
}

View File

@ -13,15 +13,9 @@ pub struct Model {
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {

View File

@ -9,7 +9,7 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
dotenv = "0.15.0"
dotenvy = "0.15.0"
async-std = { version = "^1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]

View File

@ -1,7 +1,7 @@
use sea_orm_migration::prelude::*;
#[cfg(debug_assertions)]
use dotenv::dotenv;
use dotenvy::dotenv;
#[async_std::main]
async fn main() {

View File

@ -1,19 +0,0 @@
use sea_orm::DatabaseConnection;
pub struct Database {
pub connection: DatabaseConnection,
}
impl Database {
pub async fn new() -> Self {
let connection = sea_orm::Database::connect(std::env::var("DATABASE_URL").unwrap())
.await
.expect("Could not connect to database");
Database { connection }
}
pub fn get_connection(&self) -> &DatabaseConnection {
&self.connection
}
}

View File

@ -1,49 +1,3 @@
mod db;
mod graphql;
use entity::async_graphql;
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
response::{Html, IntoResponse},
routing::get,
Router,
};
use graphql::schema::{build_schema, AppSchema};
#[cfg(debug_assertions)]
use dotenv::dotenv;
async fn graphql_handler(schema: Extension<AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new(
"/api/graphql",
)))
}
#[tokio::main]
async fn main() {
#[cfg(debug_assertions)]
dotenv().ok();
let schema = build_schema().await;
let app = Router::new()
.route(
"/api/graphql",
get(graphql_playground).post(graphql_handler),
)
.layer(Extension(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();
fn main() {
graphql_example_api::main();
}

View File

@ -6,28 +6,7 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = [".", "entity", "migration"]
members = [".", "api", "core", "entity", "migration"]
[dependencies]
jsonrpsee = { version = "^0.8.0", features = ["full"] }
jsonrpsee-core = "0.9.0"
tokio = { version = "1.8.0", features = ["full"] }
serde = { version = "1", features = ["derive"] }
dotenv = "0.15"
entity = { path = "entity" }
migration = { path = "migration" }
anyhow = "1.0.52"
async-trait = "0.1.52"
log = { version = "0.4", features = ["std"] }
simplelog = "*"
[dependencies.sea-orm]
path = "../../" # remove this line in your own project
version = "^0.10.0" # sea-orm version
features = [
"debug-print",
"runtime-tokio-native-tls",
"sqlx-sqlite",
# "sqlx-postgres",
# "sqlx-mysql",
]
jsonrpsee-example-api = { path = "api" }

View File

@ -2,11 +2,11 @@
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 `Cargo.toml` (the `"sqlx-sqlite",` line)
1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line)
1. Execute `cargo run` to start the server
2. Send jsonrpc request to server
1. Send jsonrpc request to server
```shell
#insert
@ -62,3 +62,10 @@ curl --location --request POST 'http://127.0.0.1:8000' \
}'
```
Run mock test on the core logic crate:
```bash
cd core
cargo test --features mock
```

View File

@ -0,0 +1,19 @@
[package]
name = "jsonrpsee-example-api"
version = "0.1.0"
edition = "2021"
publish = false
[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 = "*"

View File

@ -0,0 +1,143 @@
use std::env;
use anyhow::anyhow;
use entity::post;
use jsonrpsee::core::{async_trait, RpcResult};
use jsonrpsee::http_server::HttpServerBuilder;
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 log::info;
use migration::{Migrator, MigratorTrait};
use simplelog::*;
use std::fmt::Display;
use std::net::SocketAddr;
use tokio::signal::ctrl_c;
use tokio::signal::unix::{signal, SignalKind};
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
#[rpc(server, client)]
trait PostRpc {
#[method(name = "Post.List")]
async fn list(
&self,
page: Option<u64>,
posts_per_page: Option<u64>,
) -> RpcResult<Vec<post::Model>>;
#[method(name = "Post.Insert")]
async fn insert(&self, p: post::Model) -> RpcResult<i32>;
#[method(name = "Post.Update")]
async fn update(&self, p: post::Model) -> RpcResult<bool>;
#[method(name = "Post.Delete")]
async fn delete(&self, id: i32) -> RpcResult<bool>;
}
struct PpcImpl {
conn: DatabaseConnection,
}
#[async_trait]
impl PostRpcServer for PpcImpl {
async fn list(
&self,
page: Option<u64>,
posts_per_page: Option<u64>,
) -> RpcResult<Vec<post::Model>> {
let page = page.unwrap_or(1);
let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
Query::find_posts_in_page(&self.conn, page, posts_per_page)
.await
.map(|(p, _)| p)
.internal_call_error()
}
async fn insert(&self, p: post::Model) -> RpcResult<i32> {
let new_post = Mutation::create_post(&self.conn, p)
.await
.internal_call_error()?;
Ok(new_post.id.unwrap())
}
async fn update(&self, p: post::Model) -> RpcResult<bool> {
Mutation::update_post_by_id(&self.conn, p.id, p)
.await
.map(|_| true)
.internal_call_error()
}
async fn delete(&self, id: i32) -> RpcResult<bool> {
Mutation::delete_post(&self.conn, id)
.await
.map(|res| res.rows_affected == 1)
.internal_call_error()
}
}
trait IntoJsonRpcResult<T> {
fn internal_call_error(self) -> RpcResult<T>;
}
impl<T, E> IntoJsonRpcResult<T> for Result<T, E>
where
E: Display,
{
fn internal_call_error(self) -> RpcResult<T> {
self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e))))
}
}
#[tokio::main]
async fn start() -> std::io::Result<()> {
let _ = TermLogger::init(
LevelFilter::Trace,
Config::default(),
TerminalMode::Mixed,
ColorChoice::Auto,
);
// 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 server = HttpServerBuilder::default()
.build(server_url.parse::<SocketAddr>().unwrap())
.unwrap();
let rpc_impl = PpcImpl { conn };
let server_addr = server.local_addr().unwrap();
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();
tokio::select! {
_ = sig_int.recv() => info!("receive SIGINT"),
_ = sig_term.recv() => info!("receive SIGTERM"),
_ = ctrl_c() => info!("receive Ctrl C"),
}
handle.stop().unwrap();
info!("Shutdown program");
Ok(())
}
pub fn main() {
let result = start();
if let Some(err) = result.err() {
println!("Error: {}", err);
}
}

View File

@ -0,0 +1,30 @@
[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.10.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"]

View File

@ -0,0 +1,7 @@
mod mutation;
mod query;
pub use mutation::*;
pub use query::*;
pub use sea_orm;

View File

@ -0,0 +1,53 @@
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, DbErr> {
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<post::Model, DbErr> {
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<DeleteResult, DbErr> {
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<DeleteResult, DbErr> {
Post::delete_many().exec(db).await
}
}

View File

@ -0,0 +1,26 @@
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<Option<post::Model>, 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<post::Model>, 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))
}
}

View File

@ -0,0 +1,79 @@
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);
}
}

Some files were not shown because too many files have changed in this diff Show More