Compare commits
14 Commits
dependabot
...
master
Author | SHA1 | Date | |
---|---|---|---|
d509ca1397 | |||
1002f0c0e5 | |||
04f0620ba4 | |||
9cc34f16bb | |||
63302fef71 | |||
fc3052630a | |||
a8ff5101be | |||
d78139c799 | |||
35cf6ebaf9 | |||
2aea60ecf2 | |||
5f1d9c1f4d | |||
23628d6f9b | |||
a192a1584e | |||
cad12e7a76 |
@ -82,9 +82,9 @@ fabric.properties
|
|||||||
|
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
/web/target/
|
crates/web/target/
|
||||||
/web/tmp/
|
crates/web/tmp/
|
||||||
/web/build/
|
crates/web/build/
|
||||||
|
|
||||||
/jirs-server/target/
|
crates/bitque-server/target/
|
||||||
/jirs-server/tmp/
|
crates/bitque-server/tmp/
|
||||||
|
12
.env
12
.env
@ -1,7 +1,7 @@
|
|||||||
DEBUG=true
|
DEBUG=true
|
||||||
RUST_LOG=debug
|
RUST_LOG=info
|
||||||
JIRS_CLIENT_PORT=80
|
BITQUE_CLIENT_PORT=80
|
||||||
JIRS_CLIENT_BIND=jirs.lvh.me
|
BITQUE_CLIENT_BIND=bitque.lvh.me
|
||||||
DATABASE_URL=postgres://postgres@localhost:5432/jirs
|
DATABASE_URL=postgres://postgres@localhost:5432/bitque
|
||||||
JIRS_SERVER_PORT=5000
|
BITQUE_SERVER_PORT=5000
|
||||||
JIRS_SERVER_BIND=0.0.0.0
|
BITQUE_SERVER_BIND=0.0.0.0
|
||||||
|
35
.gitignore
vendored
35
.gitignore
vendored
@ -1,27 +1,12 @@
|
|||||||
/target
|
/target
|
||||||
|
/crates/bitque-client/pkg
|
||||||
mail.toml
|
/crates/bitque-client/tmp
|
||||||
mail.test.toml
|
/crates/bitque-client/build
|
||||||
web.toml
|
/crates/bitque-server/target
|
||||||
web.test.toml
|
/crates/bitque-cli/target
|
||||||
db.toml
|
/crates/bitque-bat/bat
|
||||||
db.test.toml
|
/crates/highlight/bitque-highlight/build
|
||||||
fs.toml
|
/crates/bitque-client/src/location.rs
|
||||||
fs.test.toml
|
/uploads
|
||||||
highlight.toml
|
/config
|
||||||
highlight.test.toml
|
|
||||||
|
|
||||||
pkg
|
|
||||||
jirs-client/pkg
|
|
||||||
jirs-client/tmp
|
|
||||||
jirs-client/build
|
|
||||||
tmp
|
tmp
|
||||||
jirs-server/target
|
|
||||||
jirs-cli/target
|
|
||||||
jirs-bat/bat
|
|
||||||
|
|
||||||
highlight/jirs-highlight/build
|
|
||||||
uploads
|
|
||||||
config
|
|
||||||
shared/jirs-config/target
|
|
||||||
jirs-client/src/location.rs
|
|
||||||
|
@ -8,7 +8,7 @@ Use nothing other than standard rust tests.
|
|||||||
|
|
||||||
## Submitting changes
|
## Submitting changes
|
||||||
|
|
||||||
Please send a [GitHub Pull Request to jirs](https://github.com/Eraden/hirs/pull/new/master) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). When you send a pull request, we will love you forever if you include RSpec examples. We can always use more test coverage. Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
|
Please send a [GitHub Pull Request to bitque](https://github.com/Eraden/hirs/pull/new/master) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). When you send a pull request, we will love you forever if you include RSpec examples. We can always use more test coverage. Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
|
||||||
|
|
||||||
Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this:
|
Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this:
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ Start reading our code and you'll get the hang of it. We optimize for readabilit
|
|||||||
* We ALWAYS run `cargo fmt` before commit
|
* We ALWAYS run `cargo fmt` before commit
|
||||||
* We ALWAYS run `cargo clippy` before commit
|
* We ALWAYS run `cargo clippy` before commit
|
||||||
* We avoid local variables and prefer functions in theirs place
|
* We avoid local variables and prefer functions in theirs place
|
||||||
* We prefer rust over JavaScript
|
* We prefer Rust over JavaScript
|
||||||
* We avoid putting logic in view
|
* We avoid putting logic in view
|
||||||
* This is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible.
|
* This is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible.
|
||||||
|
|
||||||
|
4165
Cargo.lock
generated
4165
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@ -1,31 +1,51 @@
|
|||||||
#[package]
|
#[package]
|
||||||
#name = "jirs"
|
#name = "bitque"
|
||||||
#version = "0.1.0"
|
#version = "0.1.0"
|
||||||
#authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
#authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
#edition = "2018"
|
#edition = "2018"
|
||||||
#description = "JIRS (Simplified JIRA in Rust)"
|
#description = "JIRS (Simplified JIRA in Rust)"
|
||||||
#repository = "https://gitlab.com/adrian.wozniak/jirs"
|
#repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
#license = "MPL-2.0"
|
#license = "MPL-2.0"
|
||||||
#license-file = "./LICENSE"
|
#license-file = "./LICENSE"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"./shared/common",
|
"./crates/bitque-cli",
|
||||||
"./jirs-cli",
|
"./crates/bitque-server",
|
||||||
"./jirs-server",
|
"./crates/bitque-config",
|
||||||
"./shared/jirs-config",
|
"./crates/bitque-data",
|
||||||
"./shared/jirs-data",
|
"./crates/derive_enum_iter",
|
||||||
"./derive/derive_enum_iter",
|
"./crates/derive_enum_primitive",
|
||||||
"./derive/derive_enum_primitive",
|
"./crates/derive_enum_sql",
|
||||||
"./derive/derive_enum_sql",
|
"./crates/derive_db_execute",
|
||||||
"./derive/derive_db_execute",
|
"./crates/highlight-actor",
|
||||||
"./actors/highlight-actor",
|
"./crates/database-actor",
|
||||||
"./actors/database-actor",
|
"./crates/web-actor",
|
||||||
"./actors/web-actor",
|
"./crates/websocket-actor",
|
||||||
"./actors/websocket-actor",
|
"./crates/mail-actor",
|
||||||
"./actors/mail-actor",
|
"./crates/cloud-storage-actor",
|
||||||
"./actors/amazon-actor",
|
"./crates/filesystem-actor",
|
||||||
"./actors/filesystem-actor",
|
|
||||||
# Client
|
# Client
|
||||||
"./web"
|
"./crates/web"
|
||||||
]
|
]
|
||||||
|
exclude = [
|
||||||
|
"crates/bitque-cli",
|
||||||
|
"crates/web",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
bitque-cli = { path = "./crates/bitque-cli" }
|
||||||
|
bitque-server = { path = "./crates/bitque-server" }
|
||||||
|
bitque-config = { path = "./crates/bitque-config" }
|
||||||
|
bitque-data = { path = "./crates/bitque-data" }
|
||||||
|
derive_enum_iter = { path = "./crates/derive_enum_iter" }
|
||||||
|
derive_enum_primitive = { path = "./crates/derive_enum_primitive" }
|
||||||
|
derive_enum_sql = { path = "./crates/derive_enum_sql" }
|
||||||
|
derive_db_execute = { path = "./crates/derive_db_execute" }
|
||||||
|
highlight-actor = { path = "./crates/highlight-actor" }
|
||||||
|
database-actor = { path = "./crates/database-actor" }
|
||||||
|
web-actor = { path = "./crates/web-actor" }
|
||||||
|
websocket-actor = { path = "./crates/websocket-actor" }
|
||||||
|
mail-actor = { path = "./crates/mail-actor" }
|
||||||
|
cloud-storage-actor = { path = "./crates/cloud-storage-actor" }
|
||||||
|
filesystem-actor = { path = "./crates/filesystem-actor" }
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
FROM ubuntu:18.04
|
|
||||||
|
|
||||||
WORKDIR /app/
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl git openssl libpq-dev gcc openssl1.0 make cmake
|
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain nightly -y
|
|
||||||
RUN . $HOME/.cargo/env && \
|
|
||||||
rustup toolchain install nightly && rustup default nightly
|
|
||||||
|
|
||||||
RUN ls -al /app
|
|
||||||
CMD . $HOME/.cargo/env && \
|
|
||||||
cd ./jirs-server && \
|
|
||||||
rm -Rf ./target/debug/jirs_server && \
|
|
||||||
cargo build --bin jirs_server --release --no-default-features --features local-storage && \
|
|
||||||
cp /app/target/release/jirs_server /app/build/
|
|
40
README.md
40
README.md
@ -1,13 +1,13 @@
|
|||||||
# A simplified Jira clone built with seed.rs and actix
|
# A simplified Jira clone built with seed.rs and actix
|
||||||
|
|
||||||
![JIRS](https://raw.githubusercontent.com/Eraden/jirs/master/web/static/project-icon.svg)
|
![JIRS](https://raw.githubusercontent.com/Eraden/bitque/master/web/static/project-icon.svg)
|
||||||
|
|
||||||
Server: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/jirs/server.yml.svg)](https://builds.sr.ht/~tsumanu/jirs/server.yml?)
|
Server: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/bitque/server.yml.svg)](https://builds.sr.ht/~tsumanu/bitque/server.yml?)
|
||||||
Client: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/jirs/client.yml.svg)](https://builds.sr.ht/~tsumanu/jirs/client.yml?)
|
Client: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/bitque/client.yml.svg)](https://builds.sr.ht/~tsumanu/bitque/client.yml?)
|
||||||
|
|
||||||
Main repo: https://git.sr.ht/~tsumanu/jirs
|
Main repo: https://git.sr.ht/~tsumanu/bitque
|
||||||
|
|
||||||
Demo: https://jirs.ita-prog.pl
|
Demo: https://bitque.ita-prog.pl
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ This requires additional configuration.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[filesystem]
|
[filesystem]
|
||||||
store_path = "/var/jirs/uploads"
|
store_path = "/var/bitque/uploads"
|
||||||
client_path = "/img"
|
client_path = "/img"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ region_name = "eu-central-1"
|
|||||||
```toml
|
```toml
|
||||||
# db.toml
|
# db.toml
|
||||||
concurrency = 2
|
concurrency = 2
|
||||||
database_url = "postgres://postgres@localhost:5432/jirs"
|
database_url = "postgres://postgres@localhost:5432/bitque"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Mail Service
|
#### Mail Service
|
||||||
@ -141,15 +141,15 @@ concurrency = 2
|
|||||||
user = "apikey"
|
user = "apikey"
|
||||||
pass = "YOUR-TOKEN"
|
pass = "YOUR-TOKEN"
|
||||||
host = "smtp.sendgrid.net"
|
host = "smtp.sendgrid.net"
|
||||||
from = "contact@jirs.pl"
|
from = "contact@bitque.pl"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local variables
|
### Local variables
|
||||||
|
|
||||||
Within `jirs` directory place `.env` file with following content
|
Within `bitque` directory place `.env` file with following content
|
||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
DATABASE_URL=postgres://postgres@localhost:5432/jirs
|
DATABASE_URL=postgres://postgres@localhost:5432/bitque
|
||||||
RUST_LOG=actix_web=info,diesel=info
|
RUST_LOG=actix_web=info,diesel=info
|
||||||
JIRS_CLIENT_PORT=7000
|
JIRS_CLIENT_PORT=7000
|
||||||
JIRS_CLIENT_BIND=0.0.0.0
|
JIRS_CLIENT_BIND=0.0.0.0
|
||||||
@ -170,23 +170,23 @@ Requirements:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install diesel_cli --no-default-features --features postgres
|
cargo install diesel_cli --no-default-features --features postgres
|
||||||
export DATABASE_URL=postgres://postgres@localhost/jirs
|
export DATABASE_URL=postgres://postgres@localhost/bitque
|
||||||
diesel setup
|
diesel setup
|
||||||
diesel migration run
|
diesel migration run
|
||||||
|
|
||||||
cargo run --bin jirs_server
|
cargo run --bin bitque_server
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
cd jirs_client
|
cd bitque_client
|
||||||
./web/scripts/prod.sh
|
./web/scripts/prod.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s ./jirs.nginx /etc/nginx/sites-enabled/
|
sudo ln -s ./bitque.nginx /etc/nginx/sites-enabled/
|
||||||
sudo nginx -s reload
|
sudo nginx -s reload
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -204,10 +204,10 @@ Custom element glued with WASM
|
|||||||
* `lang` does not have callback and it's used only on `connectedCallback`
|
* `lang` does not have callback and it's used only on `connectedCallback`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<jirs-code-view lang="Rust" file-path="/some/path.rs">
|
<bitque-code-view lang="Rust" file-path="/some/path.rs">
|
||||||
struct Foo {
|
struct Foo {
|
||||||
}
|
}
|
||||||
</jirs-code-view>
|
</bitque-code-view>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported languages
|
### Supported languages
|
||||||
@ -349,3 +349,11 @@ struct Foo {
|
|||||||
* lrc
|
* lrc
|
||||||
* reStructuredText
|
* reStructuredText
|
||||||
* srt
|
* srt
|
||||||
|
|
||||||
|
## Utils
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install --locked --git https://github.com/dcchut/cargo-derivefmt --bin cargo-derivefmt
|
||||||
|
cargo install carto-sort
|
||||||
|
cargo install cargo-llvm-cov
|
||||||
|
```
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amazon-actor"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
|
||||||
edition = "2018"
|
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
|
||||||
license = "MPL-2.0"
|
|
||||||
#license-file = "../LICENSE"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "amazon_actor"
|
|
||||||
path = "./src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = { path = "../../shared/common" }
|
|
||||||
actix = { version = "0.10.0" }
|
|
||||||
bytes = { version = "1.0.0" }
|
|
||||||
|
|
||||||
serde = { version = "*" }
|
|
||||||
|
|
||||||
futures = { version = "0.3.8" }
|
|
||||||
openssl-sys = { version = "*", features = ["vendored"] }
|
|
||||||
libc = { version = "0.2.0", default-features = false }
|
|
||||||
|
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4", "v5"] }
|
|
||||||
|
|
||||||
[dependencies.jirs-config]
|
|
||||||
path = "../../shared/jirs-config"
|
|
||||||
features = ["mail", "web", "local-storage"]
|
|
||||||
|
|
||||||
# Amazon S3
|
|
||||||
[dependencies.rusoto_s3]
|
|
||||||
version = "0.47.0"
|
|
||||||
|
|
||||||
[dependencies.rusoto_core]
|
|
||||||
version = "0.47.0"
|
|
||||||
|
|
||||||
[dependencies.rusoto_signature]
|
|
||||||
version = "0.47.0"
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
|
||||||
version = "0.2.23"
|
|
||||||
features = ["tcp", "time", "rt-core", "fs"]
|
|
@ -1,88 +0,0 @@
|
|||||||
extern crate common;
|
|
||||||
use rusoto_s3::{PutObjectRequest, S3Client, S3};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum AmazonError {
|
|
||||||
UploadFailed,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AmazonExecutor;
|
|
||||||
|
|
||||||
impl Default for AmazonExecutor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix::Actor for AmazonExecutor {
|
|
||||||
type Context = actix::SyncContext<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(actix::Message)]
|
|
||||||
#[rtype(result = "Result<String, AmazonError>")]
|
|
||||||
pub struct S3PutObject {
|
|
||||||
pub source: tokio::sync::broadcast::Receiver<common::bytes::Bytes>,
|
|
||||||
pub file_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix::Handler<S3PutObject> for AmazonExecutor {
|
|
||||||
type Result = Result<String, AmazonError>;
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: S3PutObject, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
let S3PutObject {
|
|
||||||
// source,
|
|
||||||
mut source,
|
|
||||||
file_name,
|
|
||||||
} = msg;
|
|
||||||
jirs_config::amazon::config().set_variables();
|
|
||||||
|
|
||||||
tokio::runtime::Runtime::new()
|
|
||||||
.expect("Failed to start amazon agent")
|
|
||||||
.block_on(async {
|
|
||||||
let s3 = jirs_config::amazon::config();
|
|
||||||
common::log::debug!("{:?}", s3);
|
|
||||||
|
|
||||||
// TODO: Unable to upload as stream because there is no size_hint
|
|
||||||
// let stream = source
|
|
||||||
// .into_stream()
|
|
||||||
// .map_err(|_e| std::io::Error::from_raw_os_error(1));
|
|
||||||
// let stream = futures::StreamExt::map(stream, |b| {
|
|
||||||
// use common::bytes::Buf;
|
|
||||||
// ::bytes::Bytes::from(b.bytes())
|
|
||||||
// });
|
|
||||||
use common::bytes::Buf;
|
|
||||||
let mut v: Vec<u8> = vec![];
|
|
||||||
while let Ok(b) = source.recv().await {
|
|
||||||
v.extend_from_slice(b.bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = S3Client::new(s3.region());
|
|
||||||
let put_object = PutObjectRequest {
|
|
||||||
bucket: s3.bucket.clone(),
|
|
||||||
key: file_name.clone(),
|
|
||||||
// body: Some(rusoto_signature::ByteStream::new(stream)),
|
|
||||||
body: Some(v.into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let id = match client.put_object(put_object).await {
|
|
||||||
Ok(obj) => obj,
|
|
||||||
Err(e) => {
|
|
||||||
common::log::error!("{}", e);
|
|
||||||
return Err(AmazonError::UploadFailed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
common::log::debug!("{:?}", id);
|
|
||||||
Ok(aws_s3_url(file_name.as_str()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn aws_s3_url(key: &str) -> String {
|
|
||||||
let config = jirs_config::amazon::config();
|
|
||||||
format!(
|
|
||||||
"https://{bucket}.s3.{region}.amazonaws.com/{key}",
|
|
||||||
bucket = config.bucket,
|
|
||||||
region = config.region_name,
|
|
||||||
key = key
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "database-actor"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
|
||||||
edition = "2018"
|
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
|
||||||
license = "MPL-2.0"
|
|
||||||
#license-file = "../LICENSE"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "database_actor"
|
|
||||||
path = "./src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = { path = "../../shared/common" }
|
|
||||||
actix = { version = "0.10.0" }
|
|
||||||
|
|
||||||
serde = { version = "*" }
|
|
||||||
bincode = { version = "*" }
|
|
||||||
toml = { version = "*" }
|
|
||||||
|
|
||||||
futures = { version = "0.3.8" }
|
|
||||||
openssl-sys = { version = "*", features = ["vendored"] }
|
|
||||||
libc = { version = "0.2.0", default-features = false }
|
|
||||||
|
|
||||||
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
|
||||||
r2d2 = { version = ">= 0.8, < 0.9" }
|
|
||||||
|
|
||||||
dotenv = { version = "*" }
|
|
||||||
|
|
||||||
byteorder = { version = "1.0" }
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
time = { version = "0.1" }
|
|
||||||
url = { version = "2.1.0" }
|
|
||||||
percent-encoding = { version = "2.1.0" }
|
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4", "v5"] }
|
|
||||||
ipnetwork = { version = ">=0.12.2, <0.17.0" }
|
|
||||||
num-bigint = { version = ">=0.1.41, <0.3" }
|
|
||||||
num-traits = { version = "0.2" }
|
|
||||||
num-integer = { version = "0.1.32" }
|
|
||||||
bigdecimal = { version = ">= 0.0.10, <= 0.1.0" }
|
|
||||||
bitflags = { version = "1.0" }
|
|
||||||
|
|
||||||
[dependencies.jirs-config]
|
|
||||||
path = "../../shared/jirs-config"
|
|
||||||
features = ["database"]
|
|
||||||
|
|
||||||
[dependencies.jirs-data]
|
|
||||||
path = "../../shared/jirs-data"
|
|
||||||
features = ["backend"]
|
|
||||||
|
|
||||||
[dependencies.derive_db_execute]
|
|
||||||
path = "../../derive/derive_db_execute"
|
|
||||||
|
|
||||||
[dependencies.diesel]
|
|
||||||
version = "1.4.8"
|
|
||||||
features = [ "postgres", "numeric", "uuidv07", "r2d2", "chrono" ]
|
|
@ -1,109 +0,0 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate diesel;
|
|
||||||
|
|
||||||
use actix::{Actor, SyncContext};
|
|
||||||
use diesel::pg::PgConnection;
|
|
||||||
use diesel::r2d2::{self, ConnectionManager};
|
|
||||||
pub use errors::*;
|
|
||||||
|
|
||||||
pub mod authorize_user;
|
|
||||||
pub mod comments;
|
|
||||||
pub mod epics;
|
|
||||||
pub mod errors;
|
|
||||||
pub mod invitations;
|
|
||||||
pub mod issue_assignees;
|
|
||||||
pub mod issue_statuses;
|
|
||||||
pub mod issues;
|
|
||||||
pub mod messages;
|
|
||||||
pub mod models;
|
|
||||||
pub mod prelude;
|
|
||||||
pub mod projects;
|
|
||||||
pub mod schema;
|
|
||||||
pub mod tokens;
|
|
||||||
pub mod user_projects;
|
|
||||||
pub mod user_settings;
|
|
||||||
pub mod users;
|
|
||||||
|
|
||||||
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
|
||||||
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
|
||||||
|
|
||||||
pub struct DbExecutor {
|
|
||||||
pub pool: DbPool,
|
|
||||||
pub config: jirs_config::database::Configuration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for DbExecutor {
|
|
||||||
type Context = SyncContext<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DbExecutor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
pool: build_pool(),
|
|
||||||
config: jirs_config::database::Configuration::read(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_pool() -> DbPool {
|
|
||||||
dotenv::dotenv().ok();
|
|
||||||
let config = jirs_config::database::Configuration::read();
|
|
||||||
|
|
||||||
let manager = ConnectionManager::<PgConnection>::new(&config.database_url);
|
|
||||||
r2d2::Pool::builder()
|
|
||||||
.max_size(config.concurrency as u32)
|
|
||||||
.build(manager)
|
|
||||||
.unwrap_or_else(|e| panic!("Failed to create pool. {}", e))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SyncQuery {
|
|
||||||
type Result;
|
|
||||||
|
|
||||||
fn handle(&self, pool: &DbPool) -> Self::Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Guard<'l> {
|
|
||||||
conn: &'l crate::DbPooledConn,
|
|
||||||
tm: &'l diesel::connection::AnsiTransactionManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'l> Guard<'l> {
|
|
||||||
pub fn new(conn: &'l DbPooledConn) -> Result<Self, crate::DatabaseError> {
|
|
||||||
use diesel::connection::TransactionManager;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
let tm = conn.transaction_manager();
|
|
||||||
tm.begin_transaction(conn).map_err(|e| {
|
|
||||||
common::log::error!("{:?}", e);
|
|
||||||
crate::DatabaseError::DatabaseConnectionLost
|
|
||||||
})?;
|
|
||||||
Ok(Self { conn, tm })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run<R, F: FnOnce(&Guard) -> Result<R, crate::DatabaseError>>(
|
|
||||||
&self,
|
|
||||||
f: F,
|
|
||||||
) -> Result<R, crate::DatabaseError> {
|
|
||||||
use diesel::connection::TransactionManager;
|
|
||||||
|
|
||||||
let r = f(self);
|
|
||||||
match r {
|
|
||||||
Ok(r) => {
|
|
||||||
self.tm.commit_transaction(self.conn).map_err(|e| {
|
|
||||||
common::log::error!("{:?}", e);
|
|
||||||
crate::DatabaseError::DatabaseConnectionLost
|
|
||||||
})?;
|
|
||||||
Ok(r)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
common::log::error!("{:?}", e);
|
|
||||||
self.tm.rollback_transaction(self.conn).map_err(|e| {
|
|
||||||
common::log::error!("{:?}", e);
|
|
||||||
crate::DatabaseError::DatabaseConnectionLost
|
|
||||||
})?;
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,730 +0,0 @@
|
|||||||
#![allow(unused_imports, dead_code)]
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `comments` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
comments (id) {
|
|
||||||
/// The `id` column of the `comments` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `body` column of the `comments` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
body -> Text,
|
|
||||||
/// The `user_id` column of the `comments` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_id -> Int4,
|
|
||||||
/// The `issue_id` column of the `comments` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issue_id -> Int4,
|
|
||||||
/// The `created_at` column of the `comments` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `comments` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `epics` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
epics (id) {
|
|
||||||
/// The `id` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `name` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
name -> Text,
|
|
||||||
/// The `user_id` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_id -> Int4,
|
|
||||||
/// The `project_id` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
project_id -> Int4,
|
|
||||||
/// The `created_at` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
/// The `starts_at` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Timestamp>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
starts_at -> Nullable<Timestamp>,
|
|
||||||
/// The `ends_at` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Timestamp>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
ends_at -> Nullable<Timestamp>,
|
|
||||||
/// The `description` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Text>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
description -> Nullable<Text>,
|
|
||||||
/// The `description_html` column of the `epics` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Text>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
description_html -> Nullable<Text>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
invitations (id) {
|
|
||||||
/// The `id` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `name` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
name -> Text,
|
|
||||||
/// The `email` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
email -> Text,
|
|
||||||
/// The `state` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `InvitationStateType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
state -> InvitationStateType,
|
|
||||||
/// The `project_id` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
project_id -> Int4,
|
|
||||||
/// The `invited_by_id` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
invited_by_id -> Int4,
|
|
||||||
/// The `created_at` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
/// The `bind_token` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Uuid`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
bind_token -> Uuid,
|
|
||||||
/// The `role` column of the `invitations` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `UserRoleType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
role -> UserRoleType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `issue_assignees` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issue_assignees (id) {
|
|
||||||
/// The `id` column of the `issue_assignees` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `issue_id` column of the `issue_assignees` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issue_id -> Int4,
|
|
||||||
/// The `user_id` column of the `issue_assignees` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_id -> Int4,
|
|
||||||
/// The `created_at` column of the `issue_assignees` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `issue_assignees` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issue_statuses (id) {
|
|
||||||
/// The `id` column of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `name` column of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Varchar`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
name -> Varchar,
|
|
||||||
/// The `position` column of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
position -> Int4,
|
|
||||||
/// The `project_id` column of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
project_id -> Int4,
|
|
||||||
/// The `created_at` column of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `issue_statuses` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `issues` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issues (id) {
|
|
||||||
/// The `id` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `title` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
title -> Text,
|
|
||||||
/// The `issue_type` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `IssueTypeType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issue_type -> IssueTypeType,
|
|
||||||
/// The `priority` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `IssuePriorityType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
priority -> IssuePriorityType,
|
|
||||||
/// The `list_position` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
list_position -> Int4,
|
|
||||||
/// The `description` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Text>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
description -> Nullable<Text>,
|
|
||||||
/// The `description_text` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Text>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
description_text -> Nullable<Text>,
|
|
||||||
/// The `estimate` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Int4>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
estimate -> Nullable<Int4>,
|
|
||||||
/// The `time_spent` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Int4>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
time_spent -> Nullable<Int4>,
|
|
||||||
/// The `time_remaining` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Int4>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
time_remaining -> Nullable<Int4>,
|
|
||||||
/// The `reporter_id` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
reporter_id -> Int4,
|
|
||||||
/// The `project_id` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
project_id -> Int4,
|
|
||||||
/// The `created_at` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
/// The `issue_status_id` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
issue_status_id -> Int4,
|
|
||||||
/// The `epic_id` column of the `issues` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Int4>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
epic_id -> Nullable<Int4>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `messages` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
messages (id) {
|
|
||||||
/// The `id` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `receiver_id` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
receiver_id -> Int4,
|
|
||||||
/// The `sender_id` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
sender_id -> Int4,
|
|
||||||
/// The `summary` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
summary -> Text,
|
|
||||||
/// The `description` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
description -> Text,
|
|
||||||
/// The `message_type` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `MessageTypeType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
message_type -> MessageTypeType,
|
|
||||||
/// The `hyper_link` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
hyper_link -> Text,
|
|
||||||
/// The `created_at` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `messages` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `projects` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
projects (id) {
|
|
||||||
/// The `id` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `name` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
name -> Text,
|
|
||||||
/// The `url` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
url -> Text,
|
|
||||||
/// The `description` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
description -> Text,
|
|
||||||
/// The `category` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `ProjectCategoryType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
category -> ProjectCategoryType,
|
|
||||||
/// The `created_at` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
/// The `time_tracking` column of the `projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `TimeTrackingType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
time_tracking -> TimeTrackingType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
tokens (id) {
|
|
||||||
/// The `id` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `user_id` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_id -> Int4,
|
|
||||||
/// The `access_token` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Uuid`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
access_token -> Uuid,
|
|
||||||
/// The `refresh_token` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Uuid`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
refresh_token -> Uuid,
|
|
||||||
/// The `created_at` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
/// The `bind_token` column of the `tokens` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Uuid>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
bind_token -> Nullable<Uuid>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_projects (id) {
|
|
||||||
/// The `id` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `user_id` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_id -> Int4,
|
|
||||||
/// The `project_id` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
project_id -> Int4,
|
|
||||||
/// The `is_default` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Bool`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
is_default -> Bool,
|
|
||||||
/// The `is_current` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Bool`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
is_current -> Bool,
|
|
||||||
/// The `role` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `UserRoleType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
role -> UserRoleType,
|
|
||||||
/// The `created_at` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `user_projects` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `user_settings` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_settings (id) {
|
|
||||||
/// The `id` column of the `user_settings` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `user_id` column of the `user_settings` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
user_id -> Int4,
|
|
||||||
/// The `text_editor_mode` column of the `user_settings` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `TextEditorModeType`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
text_editor_mode -> TextEditorModeType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
use diesel::sql_types::*;
|
|
||||||
use jirs_data::*;
|
|
||||||
|
|
||||||
/// Representation of the `users` table.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
users (id) {
|
|
||||||
/// The `id` column of the `users` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Int4`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
id -> Int4,
|
|
||||||
/// The `name` column of the `users` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
name -> Text,
|
|
||||||
/// The `email` column of the `users` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Text`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
email -> Text,
|
|
||||||
/// The `avatar_url` column of the `users` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Nullable<Text>`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
avatar_url -> Nullable<Text>,
|
|
||||||
/// The `created_at` column of the `users` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
created_at -> Timestamp,
|
|
||||||
/// The `updated_at` column of the `users` table.
|
|
||||||
///
|
|
||||||
/// Its SQL type is `Timestamp`.
|
|
||||||
///
|
|
||||||
/// (Automatically generated by Diesel.)
|
|
||||||
updated_at -> Timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
joinable!(comments -> issues (issue_id));
|
|
||||||
joinable!(comments -> users (user_id));
|
|
||||||
joinable!(epics -> projects (project_id));
|
|
||||||
joinable!(epics -> users (user_id));
|
|
||||||
joinable!(invitations -> projects (project_id));
|
|
||||||
joinable!(invitations -> users (invited_by_id));
|
|
||||||
joinable!(issue_assignees -> issues (issue_id));
|
|
||||||
joinable!(issue_assignees -> users (user_id));
|
|
||||||
joinable!(issue_statuses -> projects (project_id));
|
|
||||||
joinable!(issues -> epics (epic_id));
|
|
||||||
joinable!(issues -> issue_statuses (issue_status_id));
|
|
||||||
joinable!(issues -> projects (project_id));
|
|
||||||
joinable!(issues -> users (reporter_id));
|
|
||||||
joinable!(tokens -> users (user_id));
|
|
||||||
joinable!(user_projects -> projects (project_id));
|
|
||||||
joinable!(user_projects -> users (user_id));
|
|
||||||
joinable!(user_settings -> users (user_id));
|
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
|
||||||
comments,
|
|
||||||
epics,
|
|
||||||
invitations,
|
|
||||||
issue_assignees,
|
|
||||||
issue_statuses,
|
|
||||||
issues,
|
|
||||||
messages,
|
|
||||||
projects,
|
|
||||||
tokens,
|
|
||||||
user_projects,
|
|
||||||
user_settings,
|
|
||||||
users,
|
|
||||||
);
|
|
@ -1,28 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "filesystem-actor"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
|
||||||
edition = "2018"
|
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
|
||||||
license = "MPL-2.0"
|
|
||||||
#license-file = "../LICENSE"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "filesystem_actor"
|
|
||||||
path = "./src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = { path = "../../shared/common" }
|
|
||||||
actix = { version = "0.10.0" }
|
|
||||||
actix-files = { version = "0.5.0" }
|
|
||||||
|
|
||||||
futures = { version = "0.3.8" }
|
|
||||||
|
|
||||||
[dependencies.jirs-config]
|
|
||||||
path = "../../shared/jirs-config"
|
|
||||||
features = ["local-storage"]
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
|
||||||
version = "0.2.23"
|
|
||||||
features = ["dns"]
|
|
@ -1,24 +0,0 @@
|
|||||||
use std::io::BufRead;
|
|
||||||
|
|
||||||
use bincode::{deserialize_from, Result};
|
|
||||||
use flate2::bufread::ZlibDecoder;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
|
|
||||||
fn from_reader<T: DeserializeOwned, R: BufRead>(input: R) -> Result<T> {
|
|
||||||
let mut decoder = ZlibDecoder::new(input);
|
|
||||||
deserialize_from(&mut decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_binary<T: DeserializeOwned>(v: &[u8]) -> T {
|
|
||||||
from_reader(v).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn integrated_syntaxset() -> syntect::parsing::SyntaxSet {
|
|
||||||
from_binary(include_bytes!("./syntaxes.bin"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn integrated_themeset() -> syntect::highlighting::ThemeSet {
|
|
||||||
from_binary(include_bytes!("./themes.bin"))
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "web-actor"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
|
||||||
edition = "2018"
|
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
|
||||||
license = "MPL-2.0"
|
|
||||||
#license-file = "../LICENSE"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "web_actor"
|
|
||||||
path = "./src/lib.rs"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
local-storage = ["filesystem-actor"]
|
|
||||||
aws-s3 = ["amazon-actor"]
|
|
||||||
default = ["local-storage"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = { path = "../../shared/common" }
|
|
||||||
actix = { version = "0.10.0" }
|
|
||||||
|
|
||||||
serde = "*"
|
|
||||||
bincode = "*"
|
|
||||||
toml = { version = "*" }
|
|
||||||
|
|
||||||
actix-multipart = "*"
|
|
||||||
|
|
||||||
futures = { version = "0.3.8" }
|
|
||||||
openssl-sys = { version = "*", features = ["vendored"] }
|
|
||||||
libc = { version = "0.2.0", default-features = false }
|
|
||||||
|
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4", "v5"] }
|
|
||||||
|
|
||||||
[dependencies.jirs-config]
|
|
||||||
path = "../../shared/jirs-config"
|
|
||||||
features = ["mail", "web", "local-storage"]
|
|
||||||
|
|
||||||
[dependencies.jirs-data]
|
|
||||||
path = "../../shared/jirs-data"
|
|
||||||
features = ["backend"]
|
|
||||||
|
|
||||||
[dependencies.database-actor]
|
|
||||||
path = "../database-actor"
|
|
||||||
|
|
||||||
[dependencies.mail-actor]
|
|
||||||
path = "../mail-actor"
|
|
||||||
|
|
||||||
[dependencies.websocket-actor]
|
|
||||||
path = "../websocket-actor"
|
|
||||||
|
|
||||||
[dependencies.filesystem-actor]
|
|
||||||
path = "../filesystem-actor"
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[dependencies.amazon-actor]
|
|
||||||
path = "../amazon-actor"
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
|
||||||
version = "0.2.23"
|
|
||||||
features = ["dns"]
|
|
@ -1,159 +0,0 @@
|
|||||||
use actix::Addr;
|
|
||||||
use actix_multipart::Field;
|
|
||||||
use actix_web::http::header::ContentDisposition;
|
|
||||||
use actix_web::web::Data;
|
|
||||||
use actix_web::Error;
|
|
||||||
use common::*;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use jirs_data::UserId;
|
|
||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
|
||||||
|
|
||||||
#[cfg(all(feature = "local-storage", feature = "aws-s3"))]
|
|
||||||
pub(crate) async fn handle_image(
|
|
||||||
user_id: UserId,
|
|
||||||
mut field: Field,
|
|
||||||
disposition: ContentDisposition,
|
|
||||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
|
||||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
let filename = disposition.get_filename().unwrap();
|
|
||||||
let system_file_name = format!("{}-{}", user_id, filename);
|
|
||||||
|
|
||||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
|
||||||
|
|
||||||
let fs_fut = local_storage_write(system_file_name.clone(), fs, user_id, sender.subscribe());
|
|
||||||
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
|
||||||
let read_fut = read_form_data(&mut field, sender);
|
|
||||||
|
|
||||||
let fs_join = tokio::task::spawn(fs_fut);
|
|
||||||
let aws_join = tokio::task::spawn(aws_fut);
|
|
||||||
read_fut.await;
|
|
||||||
|
|
||||||
let mut new_link = None;
|
|
||||||
|
|
||||||
if let Ok(url) = fs_join.await {
|
|
||||||
new_link = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(url) = aws_join.await {
|
|
||||||
new_link = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(new_link.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(not(feature = "local-storage"), feature = "aws-s3"))]
|
|
||||||
pub(crate) async fn handle_image(
|
|
||||||
user_id: UserId,
|
|
||||||
mut field: Field,
|
|
||||||
disposition: ContentDisposition,
|
|
||||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
let filename = disposition.get_filename().unwrap();
|
|
||||||
let system_file_name = format!("{}-{}", user_id, filename);
|
|
||||||
|
|
||||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
|
||||||
|
|
||||||
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
|
||||||
let read_fut = read_form_data(&mut field, sender);
|
|
||||||
|
|
||||||
let aws_join = tokio::task::spawn(aws_fut);
|
|
||||||
read_fut.await;
|
|
||||||
|
|
||||||
let mut new_link = None;
|
|
||||||
|
|
||||||
if let Ok(url) = aws_join.await {
|
|
||||||
new_link = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(new_link.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(feature = "local-storage", not(feature = "aws-s3")))]
|
|
||||||
pub(crate) async fn handle_image(
|
|
||||||
user_id: UserId,
|
|
||||||
mut field: Field,
|
|
||||||
disposition: ContentDisposition,
|
|
||||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
let filename = disposition.get_filename().unwrap();
|
|
||||||
let system_file_name = format!("{}-{}", user_id, filename);
|
|
||||||
|
|
||||||
let (sender, _receiver) = tokio::sync::broadcast::channel(64);
|
|
||||||
|
|
||||||
let fs_fut = local_storage_write(system_file_name, fs, user_id, sender.subscribe());
|
|
||||||
let read_fut = read_form_data(&mut field, sender);
|
|
||||||
|
|
||||||
let fs_join = tokio::task::spawn(fs_fut);
|
|
||||||
read_fut.await;
|
|
||||||
|
|
||||||
let mut new_link = None;
|
|
||||||
|
|
||||||
if let Ok(url) = fs_join.await {
|
|
||||||
new_link = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(new_link.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read file from client
|
|
||||||
async fn read_form_data(field: &mut Field, sender: Sender<common::bytes::Bytes>) {
|
|
||||||
while let Some(chunk) = field.next().await {
|
|
||||||
let data = chunk.unwrap();
|
|
||||||
if let Err(err) = sender.send(data) {
|
|
||||||
log::error!("{:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stream bytes directly to AWS S3 Service
|
|
||||||
#[cfg(feature = "aws-s3")]
|
|
||||||
async fn aws_s3(
|
|
||||||
system_file_name: String,
|
|
||||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
|
||||||
receiver: Receiver<bytes::Bytes>,
|
|
||||||
) -> Option<String> {
|
|
||||||
let s3 = jirs_config::amazon::config();
|
|
||||||
if !s3.active {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
match amazon
|
|
||||||
.send(amazon_actor::S3PutObject {
|
|
||||||
source: receiver,
|
|
||||||
file_name: system_file_name.to_string(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(s)) => Some(s),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "local-storage")]
|
|
||||||
async fn local_storage_write(
|
|
||||||
system_file_name: String,
|
|
||||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
|
||||||
_user_id: jirs_data::UserId,
|
|
||||||
receiver: Receiver<bytes::Bytes>,
|
|
||||||
) -> Option<String> {
|
|
||||||
let web_config = jirs_config::web::config();
|
|
||||||
let fs_config = jirs_config::fs::config();
|
|
||||||
|
|
||||||
match fs
|
|
||||||
.send(filesystem_actor::CreateFile {
|
|
||||||
source: receiver,
|
|
||||||
file_name: system_file_name.to_string(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(_)) => Some(format!(
|
|
||||||
"{addr}{client_path}/{filename}",
|
|
||||||
addr = web_config.full_addr(),
|
|
||||||
client_path = fs_config.client_path,
|
|
||||||
filename = system_file_name
|
|
||||||
)),
|
|
||||||
Ok(_) => None,
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
11
crates/bitque-cli/Cargo.toml
Normal file
11
crates/bitque-cli/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "bitquec"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0.13.0" }
|
||||||
|
clap = { version = "4.1.13" }
|
||||||
|
termion = { version = "*" }
|
||||||
|
tui = { version = "0.19.0", features = ["termion"] }
|
@ -5,16 +5,15 @@ use std::sync::{mpsc, Arc};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use termion::event::Key;
|
use termion::event::Key;
|
||||||
use termion::input::MouseTerminal;
|
use termion::screen::IntoAlternateScreen;
|
||||||
use termion::raw::IntoRawMode;
|
|
||||||
use termion::screen::AlternateScreen;
|
|
||||||
use tui::backend::TermionBackend;
|
use tui::backend::TermionBackend;
|
||||||
use tui::layout::{Constraint, Direction, Layout};
|
use tui::layout::{Constraint, Direction, Layout};
|
||||||
use tui::style::{Color, Style};
|
use tui::style::{Color, Style};
|
||||||
|
use tui::text::{Span, Spans};
|
||||||
use tui::widgets::{Block, Borders, Tabs};
|
use tui::widgets::{Block, Borders, Tabs};
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub exit_key: Key,
|
pub exit_key: Key,
|
||||||
pub tick_rate: Duration,
|
pub tick_rate: Duration,
|
||||||
@ -126,9 +125,7 @@ struct App<'a> {
|
|||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// Terminal initialization
|
// Terminal initialization
|
||||||
let stdout = io::stdout().into_raw_mode()?;
|
let stdout = io::stdout().into_alternate_screen().unwrap();
|
||||||
let stdout = MouseTerminal::from(stdout);
|
|
||||||
let stdout = AlternateScreen::from(stdout);
|
|
||||||
let backend = TermionBackend::new(stdout);
|
let backend = TermionBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
terminal.hide_cursor()?;
|
terminal.hide_cursor()?;
|
||||||
@ -142,7 +139,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|mut f| {
|
terminal.draw(|f| {
|
||||||
let size = f.size();
|
let size = f.size();
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@ -152,12 +149,17 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let block = Block::default().style(Style::default().bg(Color::White));
|
let block = Block::default().style(Style::default().bg(Color::White));
|
||||||
f.render_widget(block, size);
|
f.render_widget(block, size);
|
||||||
let tabs = Tabs::default()
|
let tabs = Tabs::new(
|
||||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
app.tabs
|
||||||
.titles(&app.tabs.titles)
|
.titles
|
||||||
.select(app.tabs.index)
|
.iter()
|
||||||
.style(Style::default().fg(Color::Cyan))
|
.map(|s| Spans::from(vec![Span::styled(*s, Style::default())]))
|
||||||
.highlight_style(Style::default().fg(Color::Yellow));
|
.collect(),
|
||||||
|
)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||||
|
.select(app.tabs.index)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.highlight_style(Style::default().fg(Color::Yellow));
|
||||||
f.render_widget(tabs, chunks[0]);
|
f.render_widget(tabs, chunks[0]);
|
||||||
let inner = match app.tabs.index {
|
let inner = match app.tabs.index {
|
||||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
29
crates/bitque-config/Cargo.toml
Normal file
29
crates/bitque-config/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "bitque-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "bitque_config"
|
||||||
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cloud-storage = ["rust-s3"]
|
||||||
|
local-storage = []
|
||||||
|
database = []
|
||||||
|
hi = []
|
||||||
|
mail = []
|
||||||
|
web = ["cloud-storage", "local-storage"]
|
||||||
|
websocket = []
|
||||||
|
default = ["local-storage", "database", "hi", "mail", "web", "websocket"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "*" }
|
||||||
|
toml = { version = "*" }
|
||||||
|
rust-s3 = { version = "*", optional = true }
|
||||||
|
tracing = { version = "*" }
|
@ -1,7 +1,10 @@
|
|||||||
use rusoto_signature::Region;
|
use std::str::FromStr;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
use s3::Region;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub access_key_id: String,
|
pub access_key_id: String,
|
||||||
pub secret_access_key: String,
|
pub secret_access_key: String,
|
||||||
@ -18,7 +21,11 @@ impl Default for Configuration {
|
|||||||
access_key_id: "".to_string(),
|
access_key_id: "".to_string(),
|
||||||
secret_access_key: "".to_string(),
|
secret_access_key: "".to_string(),
|
||||||
bucket: "".to_string(),
|
bucket: "".to_string(),
|
||||||
region_name: Region::default().name().to_string(),
|
region_name: Region::Custom {
|
||||||
|
region: "http://localhost:9000".to_string(),
|
||||||
|
endpoint: "http://localhost:9000".to_string(),
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
active: true,
|
active: true,
|
||||||
concurrency: 2,
|
concurrency: 2,
|
||||||
}
|
}
|
||||||
@ -35,7 +42,10 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn region(&self) -> Region {
|
pub fn region(&self) -> Region {
|
||||||
self.region_name.parse::<Region>().unwrap_or_default()
|
self.region_name.parse::<Region>().unwrap_or_else(|e| {
|
||||||
|
warn!("{e}");
|
||||||
|
Region::from_str("http://localhost:9000").expect("Parse S3 Region is infallible")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_variables(&self) {
|
pub fn set_variables(&self) {
|
@ -1,4 +1,4 @@
|
|||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub concurrency: usize,
|
pub concurrency: usize,
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
@ -7,10 +7,10 @@ pub struct Configuration {
|
|||||||
impl Default for Configuration {
|
impl Default for Configuration {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let database_url = if cfg!(test) {
|
let database_url = if cfg!(test) {
|
||||||
"postgres://postgres@localhost:5432/jirs_test".to_string()
|
"postgres://postgres@localhost:5432/bitque_test".to_string()
|
||||||
} else {
|
} else {
|
||||||
std::env::var("DATABASE_URL")
|
std::env::var("DATABASE_URL")
|
||||||
.unwrap_or_else(|_| "postgres://postgres@localhost:5432/jirs".to_string())
|
.unwrap_or_else(|_| "postgres://postgres@localhost:5432/bitque".to_string())
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
concurrency: 2,
|
concurrency: 2,
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub store_path: String,
|
pub store_path: String,
|
||||||
pub client_path: String,
|
pub client_path: String,
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub concurrency: usize,
|
pub concurrency: usize,
|
||||||
#[serde(default = "Configuration::default_theme")]
|
#[serde(default = "Configuration::default_theme")]
|
@ -1,5 +1,5 @@
|
|||||||
#[cfg(feature = "aws-s3")]
|
#[cfg(feature = "cloud-storage")]
|
||||||
pub mod amazon;
|
pub mod cloud_storage;
|
||||||
|
|
||||||
#[cfg(feature = "database")]
|
#[cfg(feature = "database")]
|
||||||
pub mod database;
|
pub mod database;
|
@ -1,4 +1,4 @@
|
|||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub concurrency: usize,
|
pub concurrency: usize,
|
||||||
pub user: String,
|
pub user: String,
|
||||||
@ -14,7 +14,7 @@ impl Default for Configuration {
|
|||||||
user: "apikey".to_string(),
|
user: "apikey".to_string(),
|
||||||
pass: "YOUR-TOKEN".to_string(),
|
pass: "YOUR-TOKEN".to_string(),
|
||||||
host: "smtp.sendgrid.net".to_string(),
|
host: "smtp.sendgrid.net".to_string(),
|
||||||
from: "contact@jirs.pl".to_string(),
|
from: "contact@bitque.pl".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ pub enum Protocol {
|
|||||||
Https,
|
Https,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub concurrency: usize,
|
pub concurrency: usize,
|
||||||
pub port: String,
|
pub port: String,
|
@ -1,4 +1,4 @@
|
|||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub concurrency: usize,
|
pub concurrency: usize,
|
||||||
}
|
}
|
30
crates/bitque-data/Cargo.toml
Normal file
30
crates/bitque-data/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "bitque-data"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "bitque_data"
|
||||||
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
backend = ["diesel", "actix", "diesel-derive-newtype"]
|
||||||
|
frontend = ['derive_enum_primitive']
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0.13.0", optional = true }
|
||||||
|
chrono = { version = "*", features = ["serde"] }
|
||||||
|
diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2"], optional = true }
|
||||||
|
diesel-derive-enum = { version = "2.0.1", features = ["postgres"] }
|
||||||
|
derive_enum_primitive = { workspace = true, optional = true }
|
||||||
|
diesel-derive-more = { version = "1.1.3" }
|
||||||
|
diesel-derive-newtype = { version = "2.0.0-rc.0", optional = true }
|
||||||
|
serde = { version = "*" }
|
||||||
|
serde_json = { version = "*" }
|
||||||
|
strum = { version = "0.24.1", features = ['derive', 'strum_macros', 'std'] }
|
||||||
|
uuid = { version = "1.3.0", features = ["serde"] }
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum ProjectFieldId {
|
pub enum ProjectFieldId {
|
||||||
Name,
|
Name,
|
||||||
Url,
|
Url,
|
||||||
@ -11,20 +11,20 @@ pub enum ProjectFieldId {
|
|||||||
IssueStatusName,
|
IssueStatusName,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum SignInFieldId {
|
pub enum SignInFieldId {
|
||||||
Username,
|
Username,
|
||||||
Email,
|
Email,
|
||||||
Token,
|
Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum SignUpFieldId {
|
pub enum SignUpFieldId {
|
||||||
Username,
|
Username,
|
||||||
Email,
|
Email,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum UsersFieldId {
|
pub enum UsersFieldId {
|
||||||
Username,
|
Username,
|
||||||
Email,
|
Email,
|
||||||
@ -34,17 +34,17 @@ pub enum UsersFieldId {
|
|||||||
TextEditorMode,
|
TextEditorMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum InviteFieldId {
|
pub enum InviteFieldId {
|
||||||
Token,
|
Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum CommentFieldId {
|
pub enum CommentFieldId {
|
||||||
Body,
|
Body,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum IssueFieldId {
|
pub enum IssueFieldId {
|
||||||
Type,
|
Type,
|
||||||
Title,
|
Title,
|
||||||
@ -62,7 +62,7 @@ pub enum IssueFieldId {
|
|||||||
EpicEndsAt,
|
EpicEndsAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
|
||||||
pub enum EpicFieldId {
|
pub enum EpicFieldId {
|
||||||
Name,
|
Name,
|
||||||
StartsAt,
|
StartsAt,
|
@ -1,17 +1,16 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use derive_enum_iter::EnumIter;
|
use derive_enum_primitive::*;
|
||||||
use derive_enum_primitive::EnumPrimitive;
|
|
||||||
#[cfg(feature = "backend")]
|
|
||||||
use derive_enum_sql::EnumSql;
|
|
||||||
#[cfg(feature = "backend")]
|
#[cfg(feature = "backend")]
|
||||||
use diesel::*;
|
use diesel::*;
|
||||||
|
#[cfg(feature = "backend")]
|
||||||
|
use diesel_derive_enum::DbEnum;
|
||||||
pub use fields::*;
|
pub use fields::*;
|
||||||
pub use msg::WsMsg;
|
pub use msg::WsMsg;
|
||||||
pub use payloads::*;
|
pub use payloads::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::*;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub mod fields;
|
pub mod fields;
|
||||||
@ -52,58 +51,86 @@ pub type InvitationToken = Uuid;
|
|||||||
pub type StartsAt = NaiveDateTime;
|
pub type StartsAt = NaiveDateTime;
|
||||||
pub type EndsAt = NaiveDateTime;
|
pub type EndsAt = NaiveDateTime;
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumIter,
|
||||||
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "IssueTypeMapping")]
|
||||||
pub enum IssueType {
|
pub enum IssueType {
|
||||||
|
#[default]
|
||||||
Task,
|
Task,
|
||||||
Bug,
|
Bug,
|
||||||
Story,
|
Story,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for IssueType {
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
fn default() -> Self {
|
|
||||||
IssueType::Task
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for IssueType {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(self.to_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
|
||||||
#[cfg_attr(feature = "backend", sql_type = "IssuePriorityType")]
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumIter,
|
||||||
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "IssuePriorityMapping")]
|
||||||
pub enum IssuePriority {
|
pub enum IssuePriority {
|
||||||
Highest,
|
Highest,
|
||||||
High,
|
High,
|
||||||
|
#[default]
|
||||||
Medium,
|
Medium,
|
||||||
Low,
|
Low,
|
||||||
Lowest,
|
Lowest,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for IssuePriority {
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
fn default() -> Self {
|
#[derive(
|
||||||
IssuePriority::Medium
|
Clone,
|
||||||
}
|
Copy,
|
||||||
}
|
Debug,
|
||||||
|
Default,
|
||||||
impl std::fmt::Display for IssuePriority {
|
Deserialize,
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
Display,
|
||||||
f.write_str(self.to_str())
|
EnumAsStr,
|
||||||
}
|
EnumIter,
|
||||||
}
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
EnumU32,
|
||||||
#[cfg_attr(feature = "backend", sql_type = "UserRoleType")]
|
Hash,
|
||||||
#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Hash, EnumIter, EnumPrimitive)]
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
Serialize,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "UserRoleMapping")]
|
||||||
pub enum UserRole {
|
pub enum UserRole {
|
||||||
|
#[default]
|
||||||
User,
|
User,
|
||||||
Manager,
|
Manager,
|
||||||
Owner,
|
Owner,
|
||||||
@ -124,82 +151,93 @@ impl PartialOrd for UserRole {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UserRole {
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
fn default() -> Self {
|
#[cfg_attr(feature = "backend", PgType = "ProjectCategoryMapping")]
|
||||||
UserRole::User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for UserRole {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(self.to_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
|
||||||
#[cfg_attr(feature = "backend", sql_type = "ProjectCategoryType")]
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumIter,
|
||||||
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "ProjectCategoryMapping")]
|
||||||
pub enum ProjectCategory {
|
pub enum ProjectCategory {
|
||||||
|
#[default]
|
||||||
Software,
|
Software,
|
||||||
Marketing,
|
Marketing,
|
||||||
Business,
|
Business,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProjectCategory {
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
fn default() -> Self {
|
#[cfg_attr(feature = "backend", PgType = "InvitationStateMapping")]
|
||||||
ProjectCategory::Software
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ProjectCategory {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(self.to_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
|
||||||
#[cfg_attr(feature = "backend", sql_type = "InvitationStateType")]
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumIter,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "InvitationStateMapping")]
|
||||||
pub enum InvitationState {
|
pub enum InvitationState {
|
||||||
|
#[default]
|
||||||
Sent,
|
Sent,
|
||||||
Accepted,
|
Accepted,
|
||||||
Revoked,
|
Revoked,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InvitationState {
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
fn default() -> Self {
|
#[cfg_attr(feature = "backend", PgType = "TimeTrackingMapping")]
|
||||||
InvitationState::Sent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for InvitationState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(self.to_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
|
||||||
#[cfg_attr(feature = "backend", sql_type = "TimeTrackingType")]
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumIter,
|
||||||
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "TimeTrackingMapping")]
|
||||||
pub enum TimeTracking {
|
pub enum TimeTracking {
|
||||||
|
#[default]
|
||||||
Untracked,
|
Untracked,
|
||||||
Fibonacci,
|
Fibonacci,
|
||||||
Hourly,
|
Hourly,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TimeTracking {
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
fn default() -> Self {
|
|
||||||
Self::Untracked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Debug, PartialEq)]
|
|
||||||
pub struct ErrorResponse {
|
pub struct ErrorResponse {
|
||||||
pub errors: Vec<String>,
|
pub errors: Vec<String>,
|
||||||
}
|
}
|
||||||
@ -213,7 +251,7 @@ impl ErrorResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub id: ProjectId,
|
pub id: ProjectId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -231,7 +269,7 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Issue {
|
pub struct Issue {
|
||||||
pub id: EpicId,
|
pub id: EpicId,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -254,7 +292,7 @@ pub struct Issue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct IssueStatus {
|
pub struct IssueStatus {
|
||||||
pub id: IssueStatusId,
|
pub id: IssueStatusId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -265,7 +303,7 @@ pub struct IssueStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Invitation {
|
pub struct Invitation {
|
||||||
pub id: InvitationId,
|
pub id: InvitationId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -280,7 +318,7 @@ pub struct Invitation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
pub id: CommentId,
|
pub id: CommentId,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
@ -291,7 +329,7 @@ pub struct Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -308,7 +346,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct UserProject {
|
pub struct UserProject {
|
||||||
pub id: UserProjectId,
|
pub id: UserProjectId,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
@ -321,7 +359,7 @@ pub struct UserProject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
pub id: TokenId,
|
pub id: TokenId,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
@ -333,7 +371,7 @@ pub struct Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct IssueAssignee {
|
pub struct IssueAssignee {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub issue_id: EpicId,
|
pub issue_id: EpicId,
|
||||||
@ -342,31 +380,36 @@ pub struct IssueAssignee {
|
|||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
#[cfg_attr(feature = "backend", sql_type = "MessageTypeType")]
|
#[cfg_attr(feature = "backend", PgType = "MessageTypeMapping")]
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "MessageTypeMapping")]
|
||||||
pub enum MessageType {
|
pub enum MessageType {
|
||||||
ReceivedInvitation,
|
ReceivedInvitation,
|
||||||
AssignedToIssue,
|
AssignedToIssue,
|
||||||
|
#[default]
|
||||||
Mention,
|
Mention,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MessageType {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Mention
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for MessageType {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(self.to_label())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub id: MessageId,
|
pub id: MessageId,
|
||||||
pub receiver_id: UserId,
|
pub receiver_id: UserId,
|
||||||
@ -380,7 +423,7 @@ pub struct Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Epic {
|
pub struct Epic {
|
||||||
pub id: EpicId,
|
pub id: EpicId,
|
||||||
pub name: NameString,
|
pub name: NameString,
|
||||||
@ -400,7 +443,7 @@ pub static BOLD: FontStyle = 1;
|
|||||||
pub static UNDERLINE: FontStyle = 2;
|
pub static UNDERLINE: FontStyle = 2;
|
||||||
pub static ITALIC: FontStyle = 4;
|
pub static ITALIC: FontStyle = 4;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
/// Red component
|
/// Red component
|
||||||
pub r: u8,
|
pub r: u8,
|
||||||
@ -412,7 +455,7 @@ pub struct Color {
|
|||||||
pub a: u8,
|
pub a: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
/// Foreground color
|
/// Foreground color
|
||||||
pub foreground: Color,
|
pub foreground: Color,
|
||||||
@ -422,31 +465,43 @@ pub struct Style {
|
|||||||
pub font_style: FontStyle,
|
pub font_style: FontStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct HighlightedCode {
|
pub struct HighlightedCode {
|
||||||
pub parts: Vec<(Style, String)>,
|
pub parts: Vec<(Style, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))]
|
#[cfg_attr(feature = "backend", derive(DbEnum))]
|
||||||
#[cfg_attr(feature = "backend", sql_type = "TextEditorModeType")]
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive,
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Deserialize,
|
||||||
|
Display,
|
||||||
|
EnumAsStr,
|
||||||
|
EnumIter,
|
||||||
|
EnumLabel,
|
||||||
|
EnumString,
|
||||||
|
EnumU32,
|
||||||
|
Hash,
|
||||||
|
IntoStaticStr,
|
||||||
|
PartialEq,
|
||||||
|
PartialOrd,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "TextEditorModeMapping")]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
#[cfg_attr(feature = "backend", PgType = "TextEditorModeMapping")]
|
||||||
pub enum TextEditorMode {
|
pub enum TextEditorMode {
|
||||||
|
#[default]
|
||||||
MdOnly,
|
MdOnly,
|
||||||
RteOnly,
|
RteOnly,
|
||||||
Mixed,
|
Mixed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextEditorMode {
|
|
||||||
fn default() -> Self {
|
|
||||||
TextEditorMode::MdOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "backend", derive(Queryable))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct UserSetting {
|
pub struct UserSetting {
|
||||||
pub id: UserSettingId,
|
pub id: UserSettingId,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
@ -11,7 +11,7 @@ use crate::{
|
|||||||
UsernameString,
|
UsernameString,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub enum WsError {
|
pub enum WsError {
|
||||||
InvalidLoginPair,
|
InvalidLoginPair,
|
||||||
@ -119,7 +119,7 @@ impl WsError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct IssueSync {
|
pub struct IssueSync {
|
||||||
pub id: IssueId,
|
pub id: IssueId,
|
||||||
pub list_position: ListPosition,
|
pub list_position: ListPosition,
|
||||||
@ -127,7 +127,7 @@ pub struct IssueSync {
|
|||||||
pub epic_id: Option<IssueId>,
|
pub epic_id: Option<IssueId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgIssue {
|
pub enum WsMsgIssue {
|
||||||
IssueUpdate(IssueId, IssueFieldId, PayloadVariant),
|
IssueUpdate(IssueId, IssueFieldId, PayloadVariant),
|
||||||
IssueUpdated(Issue),
|
IssueUpdated(Issue),
|
||||||
@ -145,7 +145,7 @@ impl From<WsMsgIssue> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgIssueStatus {
|
pub enum WsMsgIssueStatus {
|
||||||
IssueStatusesLoad,
|
IssueStatusesLoad,
|
||||||
IssueStatusesLoaded(Vec<IssueStatus>),
|
IssueStatusesLoaded(Vec<IssueStatus>),
|
||||||
@ -163,7 +163,7 @@ impl From<WsMsgIssueStatus> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgComment {
|
pub enum WsMsgComment {
|
||||||
IssueCommentsLoad(IssueId),
|
IssueCommentsLoad(IssueId),
|
||||||
IssueCommentsLoaded(Vec<Comment>),
|
IssueCommentsLoaded(Vec<Comment>),
|
||||||
@ -181,7 +181,7 @@ impl From<WsMsgComment> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgInvitation {
|
pub enum WsMsgInvitation {
|
||||||
InvitationListLoad,
|
InvitationListLoad,
|
||||||
InvitationListLoaded(Vec<Invitation>),
|
InvitationListLoaded(Vec<Invitation>),
|
||||||
@ -218,7 +218,7 @@ impl From<WsMsgInvitation> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgEpic {
|
pub enum WsMsgEpic {
|
||||||
EpicsLoad,
|
EpicsLoad,
|
||||||
EpicsLoaded(Vec<Epic>),
|
EpicsLoaded(Vec<Epic>),
|
||||||
@ -237,7 +237,7 @@ pub enum WsMsgEpic {
|
|||||||
EpicTransform(EpicId, IssueType),
|
EpicTransform(EpicId, IssueType),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgProject {
|
pub enum WsMsgProject {
|
||||||
ProjectsLoad,
|
ProjectsLoad,
|
||||||
ProjectsLoaded(Vec<Project>),
|
ProjectsLoaded(Vec<Project>),
|
||||||
@ -255,7 +255,7 @@ impl From<WsMsgProject> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgSession {
|
pub enum WsMsgSession {
|
||||||
// auth
|
// auth
|
||||||
AuthorizeLoad(Uuid),
|
AuthorizeLoad(Uuid),
|
||||||
@ -279,7 +279,7 @@ impl From<WsMsgSession> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgUser {
|
pub enum WsMsgUser {
|
||||||
// users
|
// users
|
||||||
AvatarUrlChanged(UserId, AvatarUrl),
|
AvatarUrlChanged(UserId, AvatarUrl),
|
||||||
@ -297,7 +297,7 @@ impl From<WsMsgUser> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsgMessage {
|
pub enum WsMsgMessage {
|
||||||
// messages
|
// messages
|
||||||
MessageUpdated(Message),
|
MessageUpdated(Message),
|
||||||
@ -313,7 +313,7 @@ impl From<WsMsgMessage> for WsMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum WsMsg {
|
pub enum WsMsg {
|
||||||
Ping,
|
Ping,
|
||||||
Pong,
|
Pong,
|
@ -5,20 +5,20 @@ use crate::{
|
|||||||
ProjectId, TimeTracking, UserId,
|
ProjectId, TimeTracking, UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct CreateCommentPayload {
|
pub struct CreateCommentPayload {
|
||||||
pub user_id: Option<UserId>,
|
pub user_id: Option<UserId>,
|
||||||
pub issue_id: IssueId,
|
pub issue_id: IssueId,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct UpdateCommentPayload {
|
pub struct UpdateCommentPayload {
|
||||||
pub id: CommentId,
|
pub id: CommentId,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct CreateIssuePayload {
|
pub struct CreateIssuePayload {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
@ -35,7 +35,7 @@ pub struct CreateIssuePayload {
|
|||||||
pub epic_id: Option<EpicId>,
|
pub epic_id: Option<EpicId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub struct UpdateProjectPayload {
|
pub struct UpdateProjectPayload {
|
||||||
pub id: ProjectId,
|
pub id: ProjectId,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
@ -45,7 +45,7 @@ pub struct UpdateProjectPayload {
|
|||||||
pub time_tracking: Option<TimeTracking>,
|
pub time_tracking: Option<TimeTracking>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum PayloadVariant {
|
pub enum PayloadVariant {
|
||||||
OptionI32(Option<i32>),
|
OptionI32(Option<i32>),
|
||||||
VecI32(Vec<i32>),
|
VecI32(Vec<i32>),
|
||||||
@ -56,7 +56,7 @@ pub enum PayloadVariant {
|
|||||||
ProjectCategory(ProjectCategory),
|
ProjectCategory(ProjectCategory),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
|
||||||
pub struct UpdateIssuePayload {
|
pub struct UpdateIssuePayload {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
38
crates/bitque-server/Cargo.toml
Normal file
38
crates/bitque-server/Cargo.toml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "bitque"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) Actix server"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cloud-storage = ["cloud-storage-actor"]
|
||||||
|
local-storage = ["filesystem-actor"]
|
||||||
|
default = ["local-storage"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0" }
|
||||||
|
actix-rt = { version = "2" }
|
||||||
|
actix-web = { version = "4" }
|
||||||
|
cloud-storage-actor = { workspace = true, optional = true }
|
||||||
|
bitque-config = { workspace = true, features = ["web", "websocket", "local-storage", "hi", "database"] }
|
||||||
|
bitque-data = { workspace = true, features = ["backend"] }
|
||||||
|
database-actor = { workspace = true }
|
||||||
|
dotenv = { version = "*" }
|
||||||
|
filesystem-actor = { workspace = true, optional = true }
|
||||||
|
futures = { version = "*" }
|
||||||
|
highlight-actor = { workspace = true }
|
||||||
|
libc = { version = "0.2.0", default-features = false }
|
||||||
|
mail-actor = { workspace = true }
|
||||||
|
openssl-sys = { version = "*", features = ["vendored"] }
|
||||||
|
serde = { version = "*", features = ["derive"] }
|
||||||
|
serde_json = { version = ">=0.8.0, <2.0" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
toml = { version = "0.7.3" }
|
||||||
|
tracing = { version = "0" }
|
||||||
|
tracing-subscriber = { version = "0", features = ['env-filter', 'thread_local', 'serde_json'] }
|
||||||
|
web-actor = { workspace = true, features = ["local-storage"] }
|
||||||
|
websocket-actor = { workspace = true }
|
@ -8,12 +8,12 @@ RUN rustup toolchain install nightly && \
|
|||||||
|
|
||||||
RUN cargo install diesel_cli --no-default-features --features postgres
|
RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
ADD jirs-server /app/jirs-server
|
ADD bitque-server /app/bitque-server
|
||||||
ADD jirs-data /app/jirs-data
|
ADD bitque-data /app/bitque-data
|
||||||
|
|
||||||
RUN pacman -Sy openssl openssh pkgconf --noconfirm
|
RUN pacman -Sy openssl openssh pkgconf --noconfirm
|
||||||
RUN pkg-config --libs openssl
|
RUN pkg-config --libs openssl
|
||||||
|
|
||||||
CMD cd /app/jirs-server && \
|
CMD cd /app/bitque-server && \
|
||||||
$HOME/.cargo/bin/diesel setup --database-url=$DATABASE_URL && \
|
$HOME/.cargo/bin/diesel setup --database-url=$DATABASE_URL && \
|
||||||
cargo run --bin jirs_server
|
cargo run --bin bitque_server
|
@ -1,7 +1,6 @@
|
|||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use common::*;
|
use bitque_data::msg::WsError;
|
||||||
use jirs_data::msg::WsError;
|
use bitque_data::ErrorResponse;
|
||||||
use jirs_data::ErrorResponse;
|
|
||||||
|
|
||||||
const TOKEN_NOT_FOUND: &str = "Token not found";
|
const TOKEN_NOT_FOUND: &str = "Token not found";
|
||||||
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
|
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
|
@ -1,11 +1,11 @@
|
|||||||
#![feature(async_closure)]
|
#![feature(async_closure)]
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
extern crate common;
|
|
||||||
|
|
||||||
use actix::Actor;
|
use actix::Actor;
|
||||||
|
use actix_web::web::Data;
|
||||||
use actix_web::{App, HttpServer};
|
use actix_web::{App, HttpServer};
|
||||||
use common::*;
|
use tracing::info;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
|
||||||
@ -19,47 +19,52 @@ macro_rules! featured {
|
|||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> Result<(), String> {
|
async fn main() -> Result<(), String> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
pretty_env_logger::init();
|
tracing_subscriber::fmt::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
|
||||||
|
.finish()
|
||||||
|
.init();
|
||||||
|
|
||||||
let web_config = jirs_config::web::Configuration::read();
|
let web_config = bitque_config::web::Configuration::read();
|
||||||
|
|
||||||
let db_addr = actix::SyncArbiter::start(
|
let db_addr = actix::SyncArbiter::start(
|
||||||
jirs_config::database::Configuration::read().concurrency,
|
bitque_config::database::Configuration::read().concurrency,
|
||||||
database_actor::DbExecutor::default,
|
database_actor::DbExecutor::default,
|
||||||
);
|
);
|
||||||
let mail_addr = actix::SyncArbiter::start(
|
let mail_addr = actix::SyncArbiter::start(
|
||||||
jirs_config::mail::Configuration::read().concurrency,
|
bitque_config::mail::Configuration::read().concurrency,
|
||||||
mail_actor::MailExecutor::default,
|
mail_actor::MailExecutor::default,
|
||||||
);
|
);
|
||||||
let hi_addr = actix::SyncArbiter::start(
|
let hi_addr = actix::SyncArbiter::start(
|
||||||
jirs_config::hi::Configuration::read().concurrency,
|
bitque_config::hi::Configuration::read().concurrency,
|
||||||
highlight_actor::HighlightActor::default,
|
highlight_actor::HighlightActor::default,
|
||||||
);
|
);
|
||||||
#[cfg(feature = "local-storage")]
|
#[cfg(feature = "local-storage")]
|
||||||
let fs_addr = actix::SyncArbiter::start(
|
let fs_addr = actix::SyncArbiter::start(
|
||||||
jirs_config::fs::Configuration::read().concurrency,
|
bitque_config::fs::Configuration::read().concurrency,
|
||||||
filesystem_actor::FileSystemExecutor::default,
|
filesystem_actor::LocalStorageExecutor::default,
|
||||||
);
|
);
|
||||||
#[cfg(feature = "aws-s3")]
|
#[cfg(feature = "cloud-storage")]
|
||||||
let amazon_addr = actix::SyncArbiter::start(
|
let cloud_storage_addr = actix::SyncArbiter::start(
|
||||||
jirs_config::web::Configuration::read().concurrency,
|
bitque_config::web::Configuration::read().concurrency,
|
||||||
amazon_actor::AmazonExecutor::default,
|
cloud_storage_actor::CloudStorageExecutor::default,
|
||||||
);
|
);
|
||||||
|
|
||||||
let ws_server = websocket_actor::server::WsServer::start_default();
|
let ws_server = websocket_actor::server::WsServer::start_default();
|
||||||
|
|
||||||
|
info!("Listen at {}", web_config.bind_addr());
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let app = App::new().wrap(actix_web::middleware::Logger::default());
|
let app = App::new().wrap(actix_web::middleware::Logger::default());
|
||||||
|
|
||||||
// data step
|
// data step
|
||||||
let app = app
|
let app = app
|
||||||
.data(ws_server.clone())
|
.app_data(Data::new(ws_server.clone()))
|
||||||
.data(db_addr.clone())
|
.app_data(Data::new(db_addr.clone()))
|
||||||
.data(mail_addr.clone())
|
.app_data(Data::new(mail_addr.clone()))
|
||||||
.data(hi_addr.clone())
|
.app_data(Data::new(hi_addr.clone()))
|
||||||
.data(database_actor::build_pool());
|
.app_data(Data::new(database_actor::build_pool()));
|
||||||
featured! { app, "local-storage", app.data(fs_addr.clone()) };
|
featured! { app, "local-storage", app.app_data(Data::new(fs_addr.clone())) };
|
||||||
featured! { app, "aws-s3", app.data(amazon_addr.clone()) };
|
featured! { app, "cloud-storage", app.app_data(Data::new(cloud_storage_addr.clone())) };
|
||||||
|
|
||||||
// services step
|
// services step
|
||||||
let app = app
|
let app = app
|
19
crates/cloud-storage-actor/Cargo.toml
Normal file
19
crates/cloud-storage-actor/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "cloud-storage-actor"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0.13.0" }
|
||||||
|
bitque-config = { workspace = true, features = ["mail", "web", "local-storage"] }
|
||||||
|
bytes = { version = "1" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing = { version = "0.1.37" }
|
||||||
|
rust-s3 = { version = "*" }
|
||||||
|
aws-creds = { version = "=0.30.0", features = ['attohttpc'] }
|
||||||
|
thiserror = { version = "*" }
|
96
crates/cloud-storage-actor/src/lib.rs
Normal file
96
crates/cloud-storage-actor/src/lib.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use awscreds::Credentials;
|
||||||
|
use s3::Region;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AmazonError {
|
||||||
|
#[error("File upload to external storage failed")]
|
||||||
|
UploadFailed,
|
||||||
|
#[error("Failed to connect to bucket")]
|
||||||
|
ConnectBucket,
|
||||||
|
#[error("Malformed external storage credentials")]
|
||||||
|
Credentials,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CloudStorageExecutor;
|
||||||
|
|
||||||
|
impl Default for CloudStorageExecutor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix::Actor for CloudStorageExecutor {
|
||||||
|
type Context = actix::SyncContext<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(actix::Message)]
|
||||||
|
#[rtype(result = "Result<String, AmazonError>")]
|
||||||
|
pub struct PutObject {
|
||||||
|
pub source: tokio::sync::broadcast::Receiver<bytes::Bytes>,
|
||||||
|
pub file_name: String,
|
||||||
|
pub dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix::Handler<PutObject> for CloudStorageExecutor {
|
||||||
|
type Result = Result<String, AmazonError>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: PutObject, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let PutObject {
|
||||||
|
// source,
|
||||||
|
mut source,
|
||||||
|
file_name,
|
||||||
|
dir,
|
||||||
|
} = msg;
|
||||||
|
bitque_config::cloud_storage::config().set_variables();
|
||||||
|
|
||||||
|
tokio::runtime::Runtime::new()
|
||||||
|
.expect("Failed to start amazon agent")
|
||||||
|
.block_on(async {
|
||||||
|
let s3 = bitque_config::cloud_storage::config();
|
||||||
|
tracing::debug!("{:?}", s3);
|
||||||
|
|
||||||
|
let mut v: Vec<u8> = Vec::with_capacity(1024 * 1024 * 16);
|
||||||
|
while let Ok(b) = source.recv().await {
|
||||||
|
v.extend_from_slice(&b)
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = bitque_config::cloud_storage::config();
|
||||||
|
let bucket = s3::Bucket::new(
|
||||||
|
config.bucket.as_str(),
|
||||||
|
Region::from_str(config.region_name.as_str()).unwrap(),
|
||||||
|
Credentials::new(
|
||||||
|
Some(config.access_key_id.as_str()),
|
||||||
|
Some(config.secret_access_key.as_str()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("{e}");
|
||||||
|
AmazonError::Credentials
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("{e}");
|
||||||
|
AmazonError::ConnectBucket
|
||||||
|
})?
|
||||||
|
.with_path_style();
|
||||||
|
let put = bucket
|
||||||
|
.put_object(format!("{dir}/{file_name}").as_str(), &v)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("{e}");
|
||||||
|
AmazonError::UploadFailed
|
||||||
|
})?;
|
||||||
|
if put.status_code() >= 300 {
|
||||||
|
// Error
|
||||||
|
Err(AmazonError::UploadFailed)
|
||||||
|
} else {
|
||||||
|
Ok(format!("{}/{}", bucket.url(), file_name))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
42
crates/database-actor/Cargo.toml
Normal file
42
crates/database-actor/Cargo.toml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[package]
|
||||||
|
name = "database-actor"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "database_actor"
|
||||||
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0.13.0" }
|
||||||
|
bigdecimal = { version = "0.3.0" }
|
||||||
|
bincode = { version = "*" }
|
||||||
|
bitflags = { version = "2.0.2" }
|
||||||
|
bitque-config = { workspace = true, features = ["database"] }
|
||||||
|
bitque-data = { workspace = true, features = ["backend"] }
|
||||||
|
byteorder = { version = "1.0" }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
derive_db_execute = { workspace = true }
|
||||||
|
diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2", "chrono"] }
|
||||||
|
dotenv = { version = "*" }
|
||||||
|
futures = { version = "0.3.8" }
|
||||||
|
ipnetwork = { version = "0.20.0" }
|
||||||
|
libc = { version = "0.2.0", default-features = false }
|
||||||
|
num-bigint = { version = "0.4.3" }
|
||||||
|
num-integer = { version = "0.1.32" }
|
||||||
|
num-traits = { version = "0.2" }
|
||||||
|
openssl-sys = { version = "*", features = ["vendored"] }
|
||||||
|
percent-encoding = { version = "2.1.0" }
|
||||||
|
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
||||||
|
r2d2 = { version = ">= 0.8, < 0.9" }
|
||||||
|
serde = { version = "*" }
|
||||||
|
time = { version = "0.3.20" }
|
||||||
|
toml = { version = "*" }
|
||||||
|
tracing = { version = "0.1.37" }
|
||||||
|
url = { version = "2.1.0" }
|
||||||
|
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
|
141
crates/database-actor/schema.patch
Normal file
141
crates/database-actor/schema.patch
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
diff --git a/crates/database-actor/src/schema.rs b/crates/database-actor/src/schema.rs
|
||||||
|
index 34a35365..a8f775b2 100644
|
||||||
|
--- a/crates/database-actor/src/schema.rs
|
||||||
|
+++ b/crates/database-actor/src/schema.rs
|
||||||
|
@@ -37,19 +37,19 @@ diesel::table! {
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
invitations (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Text,
|
||||||
|
email -> Text,
|
||||||
|
- state -> InvitationState,
|
||||||
|
+ state -> InvitationStateMapping,
|
||||||
|
project_id -> Int4,
|
||||||
|
invited_by_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
bind_token -> Uuid,
|
||||||
|
- role -> UserRole,
|
||||||
|
+ role -> UserRoleMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
@@ -81,14 +81,14 @@ diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
issues (id) {
|
||||||
|
id -> Int4,
|
||||||
|
title -> Text,
|
||||||
|
- issue_type -> IssueType,
|
||||||
|
- priority -> IssuePriority,
|
||||||
|
+ issue_type -> IssueTypeMapping,
|
||||||
|
+ priority -> IssuePriorityMapping,
|
||||||
|
list_position -> Int4,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
description_text -> Nullable<Text>,
|
||||||
|
estimate -> Nullable<Int4>,
|
||||||
|
time_spent -> Nullable<Int4>,
|
||||||
|
time_remaining -> Nullable<Int4>,
|
||||||
|
@@ -108,13 +108,13 @@ diesel::table! {
|
||||||
|
messages (id) {
|
||||||
|
id -> Int4,
|
||||||
|
receiver_id -> Int4,
|
||||||
|
sender_id -> Int4,
|
||||||
|
summary -> Text,
|
||||||
|
description -> Text,
|
||||||
|
- message_type -> MessageType,
|
||||||
|
+ message_type -> MessageTypeMapping,
|
||||||
|
hyper_link -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -124,16 +124,16 @@ diesel::table! {
|
||||||
|
|
||||||
|
projects (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Text,
|
||||||
|
url -> Text,
|
||||||
|
description -> Text,
|
||||||
|
- category -> ProjectCategory,
|
||||||
|
+ category -> ProjectCategoryMapping,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
- time_tracking -> TimeTracking,
|
||||||
|
+ time_tracking -> TimeTrackingMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
@@ -156,26 +156,26 @@ diesel::table! {
|
||||||
|
user_projects (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
project_id -> Int4,
|
||||||
|
is_default -> Bool,
|
||||||
|
is_current -> Bool,
|
||||||
|
- role -> UserRole,
|
||||||
|
+ role -> UserRoleMapping,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
user_settings (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
- text_editor_mode -> TextEditorMode,
|
||||||
|
+ text_editor_mode -> TextEditorModeMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
@@ -205,20 +205,20 @@ diesel::joinable!(issues -> projects (project_id));
|
||||||
|
diesel::joinable!(issues -> users (reporter_id));
|
||||||
|
diesel::joinable!(tokens -> users (user_id));
|
||||||
|
diesel::joinable!(user_projects -> projects (project_id));
|
||||||
|
diesel::joinable!(user_projects -> users (user_id));
|
||||||
|
diesel::joinable!(user_settings -> users (user_id));
|
||||||
|
|
||||||
|
-diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
- comments,
|
||||||
|
- epics,
|
||||||
|
- invitations,
|
||||||
|
- issue_assignees,
|
||||||
|
- issue_statuses,
|
||||||
|
- issues,
|
||||||
|
- messages,
|
||||||
|
- projects,
|
||||||
|
- tokens,
|
||||||
|
- user_projects,
|
||||||
|
- user_settings,
|
||||||
|
- users,
|
||||||
|
-);
|
||||||
|
+// diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
+// comments,
|
||||||
|
+// epics,
|
||||||
|
+// invitations,
|
||||||
|
+// issue_assignees,
|
||||||
|
+// issue_statuses,
|
||||||
|
+// issues,
|
||||||
|
+// messages,
|
||||||
|
+// projects,
|
||||||
|
+// tokens,
|
||||||
|
+// user_projects,
|
||||||
|
+// user_settings,
|
||||||
|
+// users,
|
||||||
|
+// );
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::User;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::User;
|
|
||||||
|
|
||||||
use crate::db_find;
|
use crate::db_find;
|
||||||
use crate::tokens::FindAccessToken;
|
use crate::tokens::FindAccessToken;
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{Comment, CommentId, IssueId, UserId};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{Comment, CommentId, IssueId, UserId};
|
|
||||||
|
|
||||||
use crate::{db_create, db_delete, db_load, db_update};
|
use crate::{db_create, db_delete, db_load, db_update};
|
||||||
|
|
@ -1,10 +1,9 @@
|
|||||||
use derive_db_execute::Execute;
|
use bitque_data::{DescriptionString, EndsAt, Epic, EpicId, ProjectId, StartsAt};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{DescriptionString, EndsAt, Epic, EpicId, ProjectId, StartsAt};
|
|
||||||
|
|
||||||
use crate::{db_create, db_delete, db_load, db_update};
|
use crate::{db_create, db_delete, db_load, db_update};
|
||||||
|
|
||||||
#[derive(Execute)]
|
#[derive(derive_db_execute::Execute)]
|
||||||
#[db_exec(schema = "epics", result = "Epic", find = "epics.find(msg.epic_id)")]
|
#[db_exec(schema = "epics", result = "Epic", find = "epics.find(msg.epic_id)")]
|
||||||
pub struct FindEpic {
|
pub struct FindEpic {
|
||||||
pub epic_id: EpicId,
|
pub epic_id: EpicId,
|
@ -1,4 +1,4 @@
|
|||||||
use jirs_data::{EmailString, UsernameString};
|
use bitque_data::{EmailString, UsernameString};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OperationError {
|
pub enum OperationError {
|
@ -1,15 +1,15 @@
|
|||||||
use actix::{Handler, Message};
|
use actix::{Handler, Message};
|
||||||
use diesel::prelude::*;
|
use bitque_data::{
|
||||||
use jirs_data::{
|
|
||||||
EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token,
|
EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token,
|
||||||
User, UserId, UserRole, UsernameString,
|
User, UserId, UserRole, UsernameString,
|
||||||
};
|
};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use crate::tokens::CreateBindToken;
|
use crate::tokens::CreateBindToken;
|
||||||
use crate::users::{LookupUser, Register};
|
use crate::users::{LookupUser, Register};
|
||||||
use crate::{
|
use crate::{
|
||||||
db_create, db_delete, db_find, db_load, db_pool, db_update, DbExecutor, DbPooledConn,
|
db_create, db_delete, db_find, db_load, db_pool, db_update, DatabaseError, DbExecutor,
|
||||||
InvitationError,
|
DbPooledConn, InvitationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
db_find! {
|
db_find! {
|
||||||
@ -80,12 +80,12 @@ impl Handler<RevokeInvitation> for DbExecutor {
|
|||||||
type Result = Result<(), crate::DatabaseError>;
|
type Result = Result<(), crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let conn = db_pool!(self);
|
let mut conn = db_pool!(self);
|
||||||
UpdateInvitationState {
|
UpdateInvitationState {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
state: InvitationState::Revoked,
|
state: InvitationState::Revoked,
|
||||||
}
|
}
|
||||||
.execute(conn)?;
|
.execute(&mut conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,63 +95,75 @@ pub struct AcceptInvitation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AcceptInvitation {
|
impl AcceptInvitation {
|
||||||
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, crate::DatabaseError> {
|
pub fn execute(self, conn: &mut DbPooledConn) -> Result<Token, crate::DatabaseError> {
|
||||||
crate::Guard::new(conn)?.run::<Token, _>(|_guard| {
|
let mut res = Err(DatabaseError::DatabaseConnectionLost);
|
||||||
let invitation = crate::invitations::FindByBindToken {
|
conn.transaction(|conn| {
|
||||||
token: self.invitation_token,
|
res = self.exec_in_transaction(conn);
|
||||||
|
if res.is_err() {
|
||||||
|
Err(diesel::NotFound)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
.execute(conn)?;
|
})
|
||||||
|
.ok();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
if invitation.state == InvitationState::Revoked {
|
fn exec_in_transaction(self, conn: &mut DbPooledConn) -> Result<Token, crate::DatabaseError> {
|
||||||
return Err(crate::DatabaseError::Invitation(
|
let invitation = FindByBindToken {
|
||||||
InvitationError::InvitationRevoked,
|
token: self.invitation_token,
|
||||||
));
|
}
|
||||||
}
|
.execute(conn)?;
|
||||||
|
|
||||||
crate::invitations::UpdateInvitationState {
|
if invitation.state == InvitationState::Revoked {
|
||||||
id: invitation.id,
|
return Err(crate::DatabaseError::Invitation(
|
||||||
state: InvitationState::Accepted,
|
InvitationError::InvitationRevoked,
|
||||||
}
|
));
|
||||||
.execute(conn)?;
|
}
|
||||||
|
|
||||||
UpdateInvitationState {
|
UpdateInvitationState {
|
||||||
id: invitation.id,
|
id: invitation.id,
|
||||||
state: InvitationState::Accepted,
|
state: InvitationState::Accepted,
|
||||||
}
|
}
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
|
||||||
match {
|
UpdateInvitationState {
|
||||||
Register {
|
id: invitation.id,
|
||||||
name: invitation.name.clone(),
|
state: InvitationState::Accepted,
|
||||||
email: invitation.email.clone(),
|
}
|
||||||
project_id: Some(invitation.project_id),
|
.execute(conn)?;
|
||||||
role: UserRole::User,
|
|
||||||
}
|
|
||||||
.execute(conn)
|
|
||||||
} {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(crate::DatabaseError::User(crate::UserError::InvalidPair(..))) => (),
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user: User = LookupUser {
|
match {
|
||||||
|
Register {
|
||||||
name: invitation.name.clone(),
|
name: invitation.name.clone(),
|
||||||
email: invitation.email.clone(),
|
email: invitation.email.clone(),
|
||||||
|
project_id: Some(invitation.project_id),
|
||||||
|
role: UserRole::User,
|
||||||
}
|
}
|
||||||
.execute(conn)?;
|
.execute(conn)
|
||||||
CreateBindToken { user_id: user.id }.execute(conn)?;
|
} {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(crate::DatabaseError::User(crate::UserError::InvalidPair(..))) => (),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
crate::user_projects::CreateUserProject {
|
let user: User = LookupUser {
|
||||||
user_id: user.id,
|
name: invitation.name.clone(),
|
||||||
project_id: invitation.project_id,
|
email: invitation.email.clone(),
|
||||||
is_current: false,
|
}
|
||||||
is_default: false,
|
.execute(conn)?;
|
||||||
role: invitation.role,
|
CreateBindToken { user_id: user.id }.execute(conn)?;
|
||||||
}
|
|
||||||
.execute(conn)?;
|
|
||||||
|
|
||||||
crate::tokens::FindUserId { user_id: user.id }.execute(conn)
|
crate::user_projects::CreateUserProject {
|
||||||
})
|
user_id: user.id,
|
||||||
|
project_id: invitation.project_id,
|
||||||
|
is_current: false,
|
||||||
|
is_default: false,
|
||||||
|
role: invitation.role,
|
||||||
|
}
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
crate::tokens::FindUserId { user_id: user.id }.execute(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,8 +175,8 @@ impl Handler<AcceptInvitation> for DbExecutor {
|
|||||||
type Result = Result<Token, crate::DatabaseError>;
|
type Result = Result<Token, crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let conn = db_pool!(self);
|
let mut conn = db_pool!(self);
|
||||||
|
|
||||||
msg.execute(conn)
|
msg.execute(&mut conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
use diesel::expression::dsl::not;
|
use bitque_data::{IssueAssignee, IssueId, UserId};
|
||||||
|
use diesel::dsl::not;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{IssueAssignee, IssueId, UserId};
|
|
||||||
|
|
||||||
use crate::{db_create, db_delete, db_load, db_load_field};
|
use crate::{db_create, db_delete, db_load, db_load_field};
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
|
|
||||||
|
|
||||||
use crate::{db_create, db_delete, db_load, db_update};
|
use crate::{db_create, db_delete, db_load, db_update};
|
||||||
|
|
@ -1,11 +1,10 @@
|
|||||||
use derive_db_execute::Execute;
|
use bitque_data::{IssueId, IssuePriority, IssueStatusId, IssueType, ProjectId, UserId};
|
||||||
use diesel::expression::sql_literal::sql;
|
use diesel::dsl::sql;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{IssueId, IssuePriority, IssueStatusId, IssueType, ProjectId, UserId};
|
|
||||||
|
|
||||||
use crate::models::Issue;
|
use crate::models::Issue;
|
||||||
|
|
||||||
#[derive(Default, Execute)]
|
#[derive(Default, derive_db_execute::Execute)]
|
||||||
#[db_exec(
|
#[db_exec(
|
||||||
result = "Issue",
|
result = "Issue",
|
||||||
schema = "issues",
|
schema = "issues",
|
||||||
@ -15,7 +14,7 @@ pub struct LoadIssue {
|
|||||||
pub issue_id: IssueId,
|
pub issue_id: IssueId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Execute)]
|
#[derive(derive_db_execute::Execute)]
|
||||||
#[db_exec(
|
#[db_exec(
|
||||||
result = "Issue",
|
result = "Issue",
|
||||||
schema = "issues",
|
schema = "issues",
|
||||||
@ -25,28 +24,28 @@ pub struct LoadProjectIssues {
|
|||||||
pub project_id: ProjectId,
|
pub project_id: ProjectId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Execute)]
|
#[derive(Default, derive_db_execute::Execute)]
|
||||||
#[db_exec(result = "Issue", schema = "issues")]
|
#[db_exec(result = "Issue", schema = "issues")]
|
||||||
pub struct UpdateIssue {
|
pub struct UpdateIssue {
|
||||||
pub issue_id: jirs_data::IssueId,
|
pub issue_id: bitque_data::IssueId,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub issue_type: Option<IssueType>,
|
pub issue_type: Option<IssueType>,
|
||||||
pub priority: Option<IssuePriority>,
|
pub priority: Option<IssuePriority>,
|
||||||
pub list_position: Option<jirs_data::ListPosition>,
|
pub list_position: Option<bitque_data::ListPosition>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub description_text: Option<String>,
|
pub description_text: Option<String>,
|
||||||
pub estimate: Option<i32>,
|
pub estimate: Option<i32>,
|
||||||
pub time_spent: Option<i32>,
|
pub time_spent: Option<i32>,
|
||||||
pub time_remaining: Option<i32>,
|
pub time_remaining: Option<i32>,
|
||||||
pub project_id: Option<jirs_data::ProjectId>,
|
pub project_id: Option<bitque_data::ProjectId>,
|
||||||
pub user_ids: Option<Vec<jirs_data::UserId>>,
|
pub user_ids: Option<Vec<bitque_data::UserId>>,
|
||||||
pub reporter_id: Option<jirs_data::UserId>,
|
pub reporter_id: Option<bitque_data::UserId>,
|
||||||
pub issue_status_id: Option<jirs_data::IssueStatusId>,
|
pub issue_status_id: Option<bitque_data::IssueStatusId>,
|
||||||
pub epic_id: Option<Option<jirs_data::EpicId>>,
|
pub epic_id: Option<Option<bitque_data::EpicId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateIssue {
|
impl UpdateIssue {
|
||||||
fn execute(self, conn: &crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
fn execute(self, conn: &mut crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
||||||
let msg = self;
|
let msg = self;
|
||||||
use crate::schema::issues::dsl::*;
|
use crate::schema::issues::dsl::*;
|
||||||
if let Some(user_ids) = msg.user_ids {
|
if let Some(user_ids) = msg.user_ids {
|
||||||
@ -88,7 +87,7 @@ impl UpdateIssue {
|
|||||||
))
|
))
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::debug!("{:?}", e);
|
::tracing::debug!("{:?}", e);
|
||||||
crate::DatabaseError::GenericFailure(
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::OperationError::Create,
|
crate::OperationError::Create,
|
||||||
crate::ResourceKind::Issue,
|
crate::ResourceKind::Issue,
|
||||||
@ -97,7 +96,7 @@ impl UpdateIssue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Execute)]
|
#[derive(derive_db_execute::Execute)]
|
||||||
#[db_exec(
|
#[db_exec(
|
||||||
result = "Issue",
|
result = "Issue",
|
||||||
schema = "issues",
|
schema = "issues",
|
||||||
@ -112,9 +111,9 @@ pub struct DeleteIssue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod inner {
|
mod inner {
|
||||||
|
use bitque_data::{IssuePriority, IssueStatusId, IssueType};
|
||||||
use derive_db_execute::Execute;
|
use derive_db_execute::Execute;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{IssuePriority, IssueStatusId, IssueType};
|
|
||||||
|
|
||||||
use crate::models::Issue;
|
use crate::models::Issue;
|
||||||
|
|
||||||
@ -153,13 +152,13 @@ mod inner {
|
|||||||
pub estimate: Option<i32>,
|
pub estimate: Option<i32>,
|
||||||
pub time_spent: Option<i32>,
|
pub time_spent: Option<i32>,
|
||||||
pub time_remaining: Option<i32>,
|
pub time_remaining: Option<i32>,
|
||||||
pub project_id: jirs_data::ProjectId,
|
pub project_id: bitque_data::ProjectId,
|
||||||
pub reporter_id: jirs_data::UserId,
|
pub reporter_id: bitque_data::UserId,
|
||||||
pub epic_id: Option<jirs_data::EpicId>,
|
pub epic_id: Option<bitque_data::EpicId>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Execute)]
|
#[derive(derive_db_execute::Execute)]
|
||||||
#[db_exec(result = "Issue", schema = "issues")]
|
#[db_exec(result = "Issue", schema = "issues")]
|
||||||
pub struct CreateIssue {
|
pub struct CreateIssue {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -171,22 +170,24 @@ pub struct CreateIssue {
|
|||||||
pub estimate: Option<i32>,
|
pub estimate: Option<i32>,
|
||||||
pub time_spent: Option<i32>,
|
pub time_spent: Option<i32>,
|
||||||
pub time_remaining: Option<i32>,
|
pub time_remaining: Option<i32>,
|
||||||
pub project_id: jirs_data::ProjectId,
|
pub project_id: ProjectId,
|
||||||
pub reporter_id: jirs_data::UserId,
|
pub reporter_id: UserId,
|
||||||
pub user_ids: Vec<jirs_data::UserId>,
|
pub user_ids: Vec<UserId>,
|
||||||
pub epic_id: Option<jirs_data::EpicId>,
|
pub epic_id: Option<bitque_data::EpicId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateIssue {
|
impl CreateIssue {
|
||||||
fn execute(self, conn: &crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
fn execute(self, conn: &mut crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
||||||
use crate::schema::issues::dsl::*;
|
use crate::schema::issues::dsl::*;
|
||||||
let msg = self;
|
let msg = self;
|
||||||
|
|
||||||
let pos = issues
|
let pos = issues
|
||||||
.select(sql("COALESCE(max(list_position), 0) + 1"))
|
.select(sql::<diesel::sql_types::Integer>(
|
||||||
|
"COALESCE(max(list_position), 0) + 1",
|
||||||
|
))
|
||||||
.get_result::<i32>(conn)
|
.get_result::<i32>(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("resolve new issue position failed {}", e);
|
::tracing::error!("resolve new issue position failed {}", e);
|
||||||
crate::DatabaseError::Issue(crate::IssueError::BadListPosition)
|
crate::DatabaseError::Issue(crate::IssueError::BadListPosition)
|
||||||
})?;
|
})?;
|
||||||
let i_s_id: IssueStatusId = if msg.issue_status_id == 0 {
|
let i_s_id: IssueStatusId = if msg.issue_status_id == 0 {
|
||||||
@ -195,7 +196,7 @@ impl CreateIssue {
|
|||||||
}
|
}
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("Failed to find issue status. {:?}", e);
|
::tracing::error!("Failed to find issue status. {:?}", e);
|
||||||
e
|
e
|
||||||
})?
|
})?
|
||||||
.first()
|
.first()
|
||||||
@ -229,7 +230,7 @@ impl CreateIssue {
|
|||||||
}
|
}
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("Failed to insert issue. {:?}", e);
|
::tracing::error!("Failed to insert issue. {:?}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
if !assign_users.is_empty() {
|
if !assign_users.is_empty() {
|
||||||
@ -239,12 +240,12 @@ impl CreateIssue {
|
|||||||
}
|
}
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("Failed to apply multiple assignee to issue. {:?}", e);
|
::tracing::error!("Failed to apply multiple assignee to issue. {:?}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
issues.find(issue.id).get_result(conn).map_err(|e| {
|
issues.find(issue.id).get_result(conn).map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
crate::DatabaseError::GenericFailure(
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::OperationError::Create,
|
crate::OperationError::Create,
|
||||||
crate::ResourceKind::Issue,
|
crate::ResourceKind::Issue,
|
65
crates/database-actor/src/lib.rs
Normal file
65
crates/database-actor/src/lib.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use actix::{Actor, SyncContext};
|
||||||
|
use diesel::pg::PgConnection;
|
||||||
|
use diesel::r2d2::{self, ConnectionManager};
|
||||||
|
pub use errors::*;
|
||||||
|
|
||||||
|
pub mod authorize_user;
|
||||||
|
pub mod comments;
|
||||||
|
pub mod epics;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod invitations;
|
||||||
|
pub mod issue_assignees;
|
||||||
|
pub mod issue_statuses;
|
||||||
|
pub mod issues;
|
||||||
|
pub mod messages;
|
||||||
|
pub mod models;
|
||||||
|
pub mod prelude;
|
||||||
|
pub mod projects;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod tokens;
|
||||||
|
pub mod user_projects;
|
||||||
|
pub mod user_settings;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||||
|
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
pub struct DbExecutor {
|
||||||
|
pub pool: DbPool,
|
||||||
|
pub config: bitque_config::database::Configuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for DbExecutor {
|
||||||
|
type Context = SyncContext<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DbExecutor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pool: build_pool(),
|
||||||
|
config: bitque_config::database::Configuration::read(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_pool() -> DbPool {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
let config = bitque_config::database::Configuration::read();
|
||||||
|
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(&config.database_url);
|
||||||
|
r2d2::Pool::builder()
|
||||||
|
.max_size(config.concurrency as u32)
|
||||||
|
.build(manager)
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to create pool. {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SyncQuery {
|
||||||
|
type Result;
|
||||||
|
|
||||||
|
fn handle(&self, pool: &DbPool) -> Self::Result;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{BindToken, Message, MessageId, MessageType, User, UserId};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId};
|
|
||||||
|
|
||||||
use crate::users::{FindUser, LookupUser};
|
use crate::users::{FindUser, LookupUser};
|
||||||
use crate::{db_create, db_delete, db_load};
|
use crate::{db_create, db_delete, db_load};
|
@ -1,13 +1,13 @@
|
|||||||
use chrono::NaiveDateTime;
|
use bitque_data::{
|
||||||
use jirs_data::{
|
|
||||||
EpicId, InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, ProjectId,
|
EpicId, InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, ProjectId,
|
||||||
TimeTracking, UserId,
|
TimeTracking, UserId,
|
||||||
};
|
};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::schema::*;
|
use crate::schema::*;
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Deserialize, Queryable)]
|
#[derive(Debug, Deserialize, Queryable, Serialize)]
|
||||||
pub struct Issue {
|
pub struct Issue {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -27,9 +27,9 @@ pub struct Issue {
|
|||||||
pub epic_id: Option<EpicId>,
|
pub epic_id: Option<EpicId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<jirs_data::Issue> for Issue {
|
impl Into<bitque_data::Issue> for Issue {
|
||||||
fn into(self) -> jirs_data::Issue {
|
fn into(self) -> bitque_data::Issue {
|
||||||
jirs_data::Issue {
|
bitque_data::Issue {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
title: self.title,
|
title: self.title,
|
||||||
issue_type: self.issue_type,
|
issue_type: self.issue_type,
|
||||||
@ -52,8 +52,8 @@ impl Into<jirs_data::Issue> for Issue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||||
#[table_name = "issues"]
|
#[diesel(table_name = issues)]
|
||||||
pub struct CreateIssueForm {
|
pub struct CreateIssueForm {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
@ -70,15 +70,15 @@ pub struct CreateIssueForm {
|
|||||||
pub epic_id: Option<EpicId>,
|
pub epic_id: Option<EpicId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||||
#[table_name = "issue_assignees"]
|
#[diesel(table_name = issue_assignees)]
|
||||||
pub struct CreateIssueAssigneeForm {
|
pub struct CreateIssueAssigneeForm {
|
||||||
pub issue_id: i32,
|
pub issue_id: i32,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||||
#[table_name = "projects"]
|
#[diesel(table_name = projects)]
|
||||||
pub struct UpdateProjectForm {
|
pub struct UpdateProjectForm {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
@ -87,8 +87,8 @@ pub struct UpdateProjectForm {
|
|||||||
pub time_tracking: Option<TimeTracking>,
|
pub time_tracking: Option<TimeTracking>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||||
#[table_name = "projects"]
|
#[diesel(table_name = projects)]
|
||||||
pub struct CreateProjectForm {
|
pub struct CreateProjectForm {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@ -96,16 +96,16 @@ pub struct CreateProjectForm {
|
|||||||
pub category: ProjectCategory,
|
pub category: ProjectCategory,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||||
#[table_name = "users"]
|
#[diesel(table_name = users)]
|
||||||
pub struct UserForm {
|
pub struct UserForm {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub avatar_url: Option<String>,
|
pub avatar_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||||
#[table_name = "invitations"]
|
#[diesel(table_name = invitations)]
|
||||||
pub struct InvitationForm {
|
pub struct InvitationForm {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
@ -1,14 +1,14 @@
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! db_pool {
|
macro_rules! db_pool {
|
||||||
($self: expr) => {
|
($self: expr) => {
|
||||||
&$self.pool.get().map_err(|e| {
|
$self.pool.get().map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::DatabaseConnectionLost
|
$crate::DatabaseError::DatabaseConnectionLost
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
($self: expr, $pool: expr) => {
|
($self: expr, $pool: expr) => {
|
||||||
&$pool.get().map_err(|e| {
|
$pool.get().map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::DatabaseConnectionLost
|
$crate::DatabaseError::DatabaseConnectionLost
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
@ -18,7 +18,7 @@ macro_rules! db_pool {
|
|||||||
macro_rules! q {
|
macro_rules! q {
|
||||||
($q: expr) => {{
|
($q: expr) => {{
|
||||||
let q = $q;
|
let q = $q;
|
||||||
common::log::debug!(
|
::tracing::debug!(
|
||||||
"{}",
|
"{}",
|
||||||
diesel::debug_query::<diesel::pg::Pg, _>(&q).to_string()
|
diesel::debug_query::<diesel::pg::Pg, _>(&q).to_string()
|
||||||
);
|
);
|
||||||
@ -34,13 +34,13 @@ macro_rules! db_find {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $action {
|
impl $action {
|
||||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
pub fn execute(self, $conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||||
use crate::schema:: $schema ::dsl::*;
|
use crate::schema:: $schema ::dsl::*;
|
||||||
let $self = self;
|
let $self = self;
|
||||||
$crate::q!($q)
|
$crate::q!($q)
|
||||||
.first($conn)
|
.first($conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::GenericFailure(
|
$crate::DatabaseError::GenericFailure(
|
||||||
$crate::OperationError::LoadCollection,
|
$crate::OperationError::LoadCollection,
|
||||||
$crate::ResourceKind::$resource,
|
$crate::ResourceKind::$resource,
|
||||||
@ -57,8 +57,8 @@ macro_rules! db_find {
|
|||||||
type Result = Result<$resource, $crate::DatabaseError>;
|
type Result = Result<$resource, $crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let $conn = $crate::db_pool!(self);
|
let mut $conn = $crate::db_pool!(self);
|
||||||
msg.execute($conn)
|
msg.execute(&mut $conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -75,13 +75,13 @@ macro_rules! db_load {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $action {
|
impl $action {
|
||||||
pub fn execute(self, conn: &$crate::DbPooledConn) -> Result<Vec<$resource>, $crate::DatabaseError> {
|
pub fn execute(self, conn: &mut $crate::DbPooledConn) -> Result<Vec<$resource>, $crate::DatabaseError> {
|
||||||
use crate::schema:: $schema ::dsl::*;
|
use crate::schema:: $schema ::dsl::*;
|
||||||
let $self = self;
|
let $self = self;
|
||||||
$crate::q!($q)
|
$crate::q!($q)
|
||||||
.load(conn)
|
.load(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::GenericFailure(
|
$crate::DatabaseError::GenericFailure(
|
||||||
$crate::OperationError::LoadCollection,
|
$crate::OperationError::LoadCollection,
|
||||||
$crate::ResourceKind::$resource,
|
$crate::ResourceKind::$resource,
|
||||||
@ -98,9 +98,9 @@ macro_rules! db_load {
|
|||||||
type Result = Result<Vec<$resource>, $crate::DatabaseError>;
|
type Result = Result<Vec<$resource>, $crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let conn = $crate::db_pool!(self);
|
let mut conn = $crate::db_pool!(self);
|
||||||
|
|
||||||
msg.execute(conn)
|
msg.execute(&mut conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -114,13 +114,13 @@ macro_rules! db_load_field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $action {
|
impl $action {
|
||||||
pub fn execute(self, conn: &$crate::DbPooledConn) -> Result<Vec<$return_type>, $crate::DatabaseError> {
|
pub fn execute(self, conn: &mut $crate::DbPooledConn) -> Result<Vec<$return_type>, $crate::DatabaseError> {
|
||||||
use crate::schema:: $schema ::dsl::*;
|
use crate::schema:: $schema ::dsl::*;
|
||||||
let $self = self;
|
let $self = self;
|
||||||
$crate::q!($q)
|
$crate::q!($q)
|
||||||
.load(conn)
|
.load(conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::GenericFailure(
|
$crate::DatabaseError::GenericFailure(
|
||||||
$crate::OperationError::LoadCollection,
|
$crate::OperationError::LoadCollection,
|
||||||
$crate::ResourceKind::$resource,
|
$crate::ResourceKind::$resource,
|
||||||
@ -137,9 +137,9 @@ macro_rules! db_load_field {
|
|||||||
type Result = Result<Vec<$return_type>, $crate::DatabaseError>;
|
type Result = Result<Vec<$return_type>, $crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let conn = $crate::db_pool!(self);
|
let mut conn = $crate::db_pool!(self);
|
||||||
|
|
||||||
msg.execute(conn)
|
msg.execute(&mut conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -151,25 +151,34 @@ macro_rules! db_create {
|
|||||||
};
|
};
|
||||||
($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => {
|
($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => {
|
||||||
pub struct $action {
|
pub struct $action {
|
||||||
$(pub $field : $ty),+
|
$(pub $field : $ty),+
|
||||||
}
|
}
|
||||||
|
|
||||||
impl $action {
|
impl $action {
|
||||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
pub fn execute(self, conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||||
crate::Guard::new($conn)?.run(|_guard| {
|
let mut res = Err(crate::DatabaseError::DatabaseConnectionLost);
|
||||||
use crate::schema:: $schema ::dsl::*;
|
conn.transaction(|conn| {
|
||||||
let $self = self;
|
res = self.exec_with_transaction(conn);
|
||||||
$crate::q!($q)
|
if res.is_err() {
|
||||||
.get_result::<$resource>($conn)
|
Err(diesel::NotFound)
|
||||||
.map_err(|e| {
|
} else {
|
||||||
common::log::error!("{:?}", e);
|
Ok(())
|
||||||
|
}
|
||||||
|
}).ok();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exec_with_transaction(self, $conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||||
|
use crate::schema:: $schema ::dsl::*;
|
||||||
|
let $self = self;
|
||||||
|
$crate::q!($q).get_result::<$resource>($conn).map_err(|e| {
|
||||||
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::GenericFailure(
|
$crate::DatabaseError::GenericFailure(
|
||||||
$crate::OperationError::Create,
|
$crate::OperationError::Create,
|
||||||
$crate::ResourceKind::$resource,
|
$crate::ResourceKind::$resource,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix::Message for $action {
|
impl actix::Message for $action {
|
||||||
@ -180,9 +189,9 @@ macro_rules! db_create {
|
|||||||
type Result = Result<$resource, $crate::DatabaseError>;
|
type Result = Result<$resource, $crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let $conn = $crate::db_pool!(self);
|
let mut $conn = $crate::db_pool!(self);
|
||||||
|
|
||||||
msg.execute($conn)
|
msg.execute(&mut $conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -199,13 +208,13 @@ macro_rules! db_update {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $action {
|
impl $action {
|
||||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
pub fn execute(self, $conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||||
use crate::schema:: $schema ::dsl::*;
|
use crate::schema:: $schema ::dsl::*;
|
||||||
let $self = self;
|
let $self = self;
|
||||||
$crate::q!($q)
|
$crate::q!($q)
|
||||||
.get_result::<$resource>($conn)
|
.get_result::<$resource>($conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::GenericFailure(
|
$crate::DatabaseError::GenericFailure(
|
||||||
$crate::OperationError::Update,
|
$crate::OperationError::Update,
|
||||||
$crate::ResourceKind::$resource,
|
$crate::ResourceKind::$resource,
|
||||||
@ -222,9 +231,9 @@ macro_rules! db_update {
|
|||||||
type Result = Result<$resource, $crate::DatabaseError>;
|
type Result = Result<$resource, $crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let $conn = $crate::db_pool!(self);
|
let mut $conn = $crate::db_pool!(self);
|
||||||
|
|
||||||
msg.execute ( $conn )
|
msg.execute ( &mut $conn )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -241,13 +250,13 @@ macro_rules! db_delete {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $action {
|
impl $action {
|
||||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<usize, $crate::DatabaseError> {
|
pub fn execute(self, $conn: &mut $crate::DbPooledConn) -> Result<usize, $crate::DatabaseError> {
|
||||||
use $crate::schema:: $schema ::dsl::*;
|
use $crate::schema:: $schema ::dsl::*;
|
||||||
let $self = self;
|
let $self = self;
|
||||||
$crate::q!($q)
|
$crate::q!($q)
|
||||||
.execute($conn)
|
.execute($conn)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
common::log::error!("{:?}", e);
|
::tracing::error!("{:?}", e);
|
||||||
$crate::DatabaseError::GenericFailure(
|
$crate::DatabaseError::GenericFailure(
|
||||||
$crate::OperationError::Delete,
|
$crate::OperationError::Delete,
|
||||||
$crate::ResourceKind::$resource,
|
$crate::ResourceKind::$resource,
|
||||||
@ -264,9 +273,9 @@ macro_rules! db_delete {
|
|||||||
type Result = Result<usize, $crate::DatabaseError>;
|
type Result = Result<usize, $crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let $conn = $crate::db_pool!(self);
|
let mut $conn = $crate::db_pool!(self);
|
||||||
|
|
||||||
msg.execute($conn)
|
msg.execute(&mut $conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId};
|
|
||||||
|
|
||||||
use crate::{db_create, db_find, db_load, db_update};
|
use crate::{db_create, db_find, db_load, db_update};
|
||||||
|
|
||||||
@ -11,8 +11,8 @@ db_find! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod inner {
|
mod inner {
|
||||||
|
use bitque_data::{NameString, Project, ProjectCategory, TimeTracking};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{NameString, Project, ProjectCategory, TimeTracking};
|
|
||||||
|
|
||||||
use crate::db_create;
|
use crate::db_create;
|
||||||
|
|
@ -1,13 +1,13 @@
|
|||||||
diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs
|
diff --git a/bitque-server/src/schema.rs b/bitque-server/src/schema.rs
|
||||||
index 00d1c0b..5b82ccf 100644
|
index 00d1c0b..5b82ccf 100644
|
||||||
--- a/jirs-server/src/schema.rs
|
--- a/bitque-server/src/schema.rs
|
||||||
+++ b/jirs-server/src/schema.rs
|
+++ b/bitque-server/src/schema.rs
|
||||||
@@ -1,6 +1,8 @@
|
@@ -1,6 +1,8 @@
|
||||||
+#![allow(unused_imports, dead_code)]
|
+#![allow(unused_imports, dead_code)]
|
||||||
+
|
+
|
||||||
table! {
|
table! {
|
||||||
use diesel::sql_types::*;
|
use diesel::sql_types::*;
|
||||||
use jirs_data::*;
|
use bitque_data::*;
|
||||||
|
|
||||||
/// Representation of the `comments` table.
|
/// Representation of the `comments` table.
|
||||||
///
|
///
|
224
crates/database-actor/src/schema.rs
Normal file
224
crates/database-actor/src/schema.rs
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
comments (id) {
|
||||||
|
id -> Int4,
|
||||||
|
body -> Text,
|
||||||
|
user_id -> Int4,
|
||||||
|
issue_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
epics (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Text,
|
||||||
|
user_id -> Int4,
|
||||||
|
project_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
starts_at -> Nullable<Timestamp>,
|
||||||
|
ends_at -> Nullable<Timestamp>,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
description_html -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
invitations (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Text,
|
||||||
|
email -> Text,
|
||||||
|
state -> InvitationStateMapping,
|
||||||
|
project_id -> Int4,
|
||||||
|
invited_by_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
bind_token -> Uuid,
|
||||||
|
role -> UserRoleMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
issue_assignees (id) {
|
||||||
|
id -> Int4,
|
||||||
|
issue_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
issue_statuses (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
position -> Int4,
|
||||||
|
project_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
issues (id) {
|
||||||
|
id -> Int4,
|
||||||
|
title -> Text,
|
||||||
|
issue_type -> IssueTypeMapping,
|
||||||
|
priority -> IssuePriorityMapping,
|
||||||
|
list_position -> Int4,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
description_text -> Nullable<Text>,
|
||||||
|
estimate -> Nullable<Int4>,
|
||||||
|
time_spent -> Nullable<Int4>,
|
||||||
|
time_remaining -> Nullable<Int4>,
|
||||||
|
reporter_id -> Int4,
|
||||||
|
project_id -> Int4,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
issue_status_id -> Int4,
|
||||||
|
epic_id -> Nullable<Int4>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
messages (id) {
|
||||||
|
id -> Int4,
|
||||||
|
receiver_id -> Int4,
|
||||||
|
sender_id -> Int4,
|
||||||
|
summary -> Text,
|
||||||
|
description -> Text,
|
||||||
|
message_type -> MessageTypeMapping,
|
||||||
|
hyper_link -> Text,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
projects (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Text,
|
||||||
|
url -> Text,
|
||||||
|
description -> Text,
|
||||||
|
category -> ProjectCategoryMapping,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
time_tracking -> TimeTrackingMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
tokens (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
access_token -> Uuid,
|
||||||
|
refresh_token -> Uuid,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
bind_token -> Nullable<Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
user_projects (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
project_id -> Int4,
|
||||||
|
is_default -> Bool,
|
||||||
|
is_current -> Bool,
|
||||||
|
role -> UserRoleMapping,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
user_settings (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
text_editor_mode -> TextEditorModeMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use bitque_data::*;
|
||||||
|
|
||||||
|
users (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Text,
|
||||||
|
email -> Text,
|
||||||
|
avatar_url -> Nullable<Text>,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(comments -> issues (issue_id));
|
||||||
|
diesel::joinable!(comments -> users (user_id));
|
||||||
|
diesel::joinable!(epics -> projects (project_id));
|
||||||
|
diesel::joinable!(epics -> users (user_id));
|
||||||
|
diesel::joinable!(invitations -> projects (project_id));
|
||||||
|
diesel::joinable!(invitations -> users (invited_by_id));
|
||||||
|
diesel::joinable!(issue_assignees -> issues (issue_id));
|
||||||
|
diesel::joinable!(issue_assignees -> users (user_id));
|
||||||
|
diesel::joinable!(issue_statuses -> projects (project_id));
|
||||||
|
diesel::joinable!(issues -> epics (epic_id));
|
||||||
|
diesel::joinable!(issues -> issue_statuses (issue_status_id));
|
||||||
|
diesel::joinable!(issues -> projects (project_id));
|
||||||
|
diesel::joinable!(issues -> users (reporter_id));
|
||||||
|
diesel::joinable!(tokens -> users (user_id));
|
||||||
|
diesel::joinable!(user_projects -> projects (project_id));
|
||||||
|
diesel::joinable!(user_projects -> users (user_id));
|
||||||
|
diesel::joinable!(user_settings -> users (user_id));
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
comments,
|
||||||
|
epics,
|
||||||
|
invitations,
|
||||||
|
issue_assignees,
|
||||||
|
issue_statuses,
|
||||||
|
issues,
|
||||||
|
messages,
|
||||||
|
projects,
|
||||||
|
tokens,
|
||||||
|
user_projects,
|
||||||
|
user_settings,
|
||||||
|
users,
|
||||||
|
);
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{Token, UserId};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{Token, UserId};
|
|
||||||
|
|
||||||
use crate::{db_create, db_find, db_update};
|
use crate::{db_create, db_find, db_update};
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole};
|
|
||||||
|
|
||||||
use crate::{db_create, db_delete, db_find, db_load, db_update};
|
use crate::{db_create, db_delete, db_find, db_load, db_update};
|
||||||
|
|
||||||
@ -26,8 +26,8 @@ db_load! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod inner {
|
mod inner {
|
||||||
|
use bitque_data::{UserId, UserProject, UserProjectId};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{UserId, UserProject, UserProjectId};
|
|
||||||
|
|
||||||
use crate::db_update;
|
use crate::db_update;
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{TextEditorMode, UserId, UserSetting};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{TextEditorMode, UserId, UserSetting};
|
|
||||||
|
|
||||||
use crate::{db_find, db_update};
|
use crate::{db_find, db_update};
|
||||||
|
|
||||||
@ -29,8 +29,8 @@ db_update! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod inner {
|
mod inner {
|
||||||
|
use bitque_data::{TextEditorMode, UserId, UserSetting};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{TextEditorMode, UserId, UserSetting};
|
|
||||||
|
|
||||||
use crate::{db_create, db_update};
|
use crate::{db_create, db_update};
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
|
use bitque_data::{EmailString, IssueId, ProjectId, User, UserId, UserRole, UsernameString};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use jirs_data::{EmailString, IssueId, ProjectId, User, UserId, UserRole, UsernameString};
|
|
||||||
|
|
||||||
use crate::projects::CreateProject;
|
use crate::projects::CreateProject;
|
||||||
use crate::user_projects::CreateUserProject;
|
use crate::user_projects::CreateUserProject;
|
||||||
@ -120,7 +120,7 @@ db_load! {
|
|||||||
user_id => UserId
|
user_id => UserId
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 {
|
fn count_matching_users(name: &str, email: &str, conn: &mut DbPooledConn) -> i64 {
|
||||||
use crate::schema::users::dsl;
|
use crate::schema::users::dsl;
|
||||||
|
|
||||||
q!(dsl::users
|
q!(dsl::users
|
||||||
@ -153,11 +153,13 @@ db_update! {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use diesel::connection::TransactionManager;
|
use bitque_data::{Project, ProjectCategory};
|
||||||
use jirs_data::{Project, ProjectCategory};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::build_pool;
|
use crate::build_pool;
|
||||||
|
use crate::schema::issues::dsl::issues;
|
||||||
|
use crate::schema::tokens::dsl::tokens;
|
||||||
|
use crate::schema::user_settings::dsl::user_settings;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_collision() {
|
fn check_collision() {
|
||||||
@ -166,13 +168,14 @@ mod tests {
|
|||||||
use crate::schema::users::dsl::users;
|
use crate::schema::users::dsl::users;
|
||||||
|
|
||||||
let pool = build_pool();
|
let pool = build_pool();
|
||||||
let conn = &pool.get().unwrap();
|
let mut conn = pool.get().unwrap();
|
||||||
|
let conn = &mut conn;
|
||||||
let tm = conn.transaction_manager();
|
conn.begin_test_transaction().unwrap();
|
||||||
|
|
||||||
tm.begin_transaction(conn).unwrap();
|
|
||||||
|
|
||||||
|
diesel::delete(user_settings).execute(conn).unwrap();
|
||||||
diesel::delete(user_projects).execute(conn).unwrap();
|
diesel::delete(user_projects).execute(conn).unwrap();
|
||||||
|
diesel::delete(tokens).execute(conn).unwrap();
|
||||||
|
diesel::delete(issues).execute(conn).unwrap();
|
||||||
diesel::delete(users).execute(conn).unwrap();
|
diesel::delete(users).execute(conn).unwrap();
|
||||||
diesel::delete(projects).execute(conn).unwrap();
|
diesel::delete(projects).execute(conn).unwrap();
|
||||||
|
|
||||||
@ -218,8 +221,6 @@ mod tests {
|
|||||||
let res2 = count_matching_users("Bar", "foo@example.com", conn);
|
let res2 = count_matching_users("Bar", "foo@example.com", conn);
|
||||||
let res3 = count_matching_users("Foo", "foo@example.com", conn);
|
let res3 = count_matching_users("Foo", "foo@example.com", conn);
|
||||||
|
|
||||||
tm.rollback_transaction(conn).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(res1, 1);
|
assert_eq!(res1, 1);
|
||||||
assert_eq!(res2, 1);
|
assert_eq!(res2, 1);
|
||||||
assert_eq!(res3, 1);
|
assert_eq!(res3, 1);
|
@ -3,8 +3,8 @@ name = "derive_db_execute"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
@ -13,4 +13,5 @@ name = "derive_db_execute"
|
|||||||
path = "./src/lib.rs"
|
path = "./src/lib.rs"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dev-dependencies]
|
||||||
|
diesel = { version = "2", features = ['postgres'] }
|
@ -10,6 +10,9 @@ use proc_macro::{TokenStream, TokenTree};
|
|||||||
|
|
||||||
use crate::parse_attr::Attributes;
|
use crate::parse_attr::Attributes;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
fn parse_meta(mut it: Peekable<IntoIter>) -> (Peekable<IntoIter>, Option<Attributes>) {
|
fn parse_meta(mut it: Peekable<IntoIter>) -> (Peekable<IntoIter>, Option<Attributes>) {
|
||||||
let mut attrs: Option<Attributes> = None;
|
let mut attrs: Option<Attributes> = None;
|
||||||
while let Some(token) = it.peek() {
|
while let Some(token) = it.peek() {
|
||||||
@ -35,41 +38,44 @@ fn parse_meta(mut it: Peekable<IntoIter>) -> (Peekable<IntoIter>, Option<Attribu
|
|||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Example:
|
/// ## Example:
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// pub struct Issue {
|
/// use derive_db_execute::Execute;
|
||||||
/// pub id: i32,
|
|
||||||
/// pub name: String,
|
|
||||||
/// }
|
|
||||||
///
|
///
|
||||||
/// #[derive(Execute)]
|
/// pub struct Issue {
|
||||||
/// #[db_exec(schema = "issues", result = "Issue", find = "issues.find(msg.id)")]
|
/// pub id: i32,
|
||||||
/// pub struct FindOne {
|
/// pub name: String,
|
||||||
/// pub id: i32,
|
/// }
|
||||||
/// }
|
|
||||||
///
|
///
|
||||||
/// #[derive(Execute)]
|
/// #[derive(Execute)]
|
||||||
/// #[db_exec(schema = "issues", result = "Issue", load = "issues")]
|
/// #[db_exec(schema = "issues", result = "Issue", find = "issues.find(msg.id)")]
|
||||||
/// pub struct LoadAll;
|
/// pub struct FindOne {
|
||||||
|
/// pub id: i32,
|
||||||
|
/// }
|
||||||
///
|
///
|
||||||
/// #[derive(Execute)]
|
/// #[derive(Execute)]
|
||||||
/// #[db_exec(schema = "issues", result = "usize", destroy = "diesel::delete(issues.find(msg.id)")]
|
/// #[db_exec(schema = "issues", result = "Issue", load = "issues")]
|
||||||
/// pub struct DeleteOne {
|
/// pub struct LoadAll;
|
||||||
/// pub id: i32
|
|
||||||
/// }
|
|
||||||
///
|
///
|
||||||
/// #[derive(Execute)]
|
/// #[derive(Execute)]
|
||||||
/// #[db_exec(schema = "issues", result = "Issue", destroy = "diesel::insert_into(issues).values(name.eq(msg.name))")]
|
/// #[db_exec(schema = "issues", result = "usize", destroy = "diesel::delete(issues.find(msg.id))")]
|
||||||
/// pub struct CreateOne {
|
/// pub struct DeleteOne {
|
||||||
/// pub name: String
|
/// pub id: i32
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[derive(Execute)]
|
/// #[derive(Execute)]
|
||||||
/// #[db_exec(schema = "issues", result = "Issue", destroy = "diesel::update(issues.find(msg.id)).set(name.eq(msg.name))")]
|
/// #[db_exec(schema = "issues", result = "Issue", destroy = "diesel::insert_into(issues).values(name.eq(msg.name))")]
|
||||||
/// pub struct UpdateOne {
|
/// pub struct CreateOne {
|
||||||
/// pub id: i32,
|
/// pub name: String
|
||||||
/// pub name: String
|
/// }
|
||||||
/// }
|
///
|
||||||
|
/// #[derive(Execute)]
|
||||||
|
/// #[db_exec(schema = "issues", result = "Issue", destroy = "diesel::update(issues.find(msg.id)).set(name.eq(msg.name))")]
|
||||||
|
/// pub struct UpdateOne {
|
||||||
|
/// pub id: i32,
|
||||||
|
/// pub name: String
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[proc_macro_derive(Execute, attributes(db_exec))]
|
#[proc_macro_derive(Execute, attributes(db_exec))]
|
||||||
pub fn derive_enum_iter(item: TokenStream) -> TokenStream {
|
pub fn derive_enum_iter(item: TokenStream) -> TokenStream {
|
||||||
@ -124,9 +130,9 @@ pub fn derive_enum_iter(item: TokenStream) -> TokenStream {
|
|||||||
type Result = Result<{action_result}, crate::DatabaseError>;
|
type Result = Result<{action_result}, crate::DatabaseError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: {name}, _ctx: &mut Self::Context) -> Self::Result {{
|
fn handle(&mut self, msg: {name}, _ctx: &mut Self::Context) -> Self::Result {{
|
||||||
let conn = crate::db_pool!(self);
|
let mut conn = crate::db_pool!(self);
|
||||||
|
|
||||||
msg.execute(conn)
|
msg.execute(&mut conn)
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@ -151,18 +157,18 @@ fn build_create_exec(
|
|||||||
impl {name} {{
|
impl {name} {{
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
self,
|
self,
|
||||||
conn: &crate::DbPooledConn,
|
conn: &mut crate::DbPooledConn,
|
||||||
) -> Result<{action_result}, crate::DatabaseError> {{
|
) -> Result<{action_result}, crate::DatabaseError> {{
|
||||||
crate::Guard::new(conn)?.run(|_guard| {{
|
conn.transaction(|conn| {{
|
||||||
use crate::schema::{schema}::dsl::*;
|
use crate::schema::{schema}::dsl::*;
|
||||||
let msg = self;
|
let msg = self;
|
||||||
crate::q!({query}).get_result(conn).map_err(|e| {{
|
crate::q!({query}).get_result(conn)
|
||||||
common::log::error!("{{:?}}", e);
|
}}).map_err(|e| {{
|
||||||
crate::DatabaseError::GenericFailure(
|
::tracing::error!("{{:?}}", e);
|
||||||
crate::OperationError::Create,
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::ResourceKind::{resource},
|
crate::OperationError::Create,
|
||||||
)
|
crate::ResourceKind::{resource},
|
||||||
}})
|
)
|
||||||
}})
|
}})
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
@ -187,12 +193,12 @@ fn build_find_exec(
|
|||||||
impl {name} {{
|
impl {name} {{
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
self,
|
self,
|
||||||
conn: &crate::DbPooledConn,
|
conn: &mut crate::DbPooledConn,
|
||||||
) -> Result<{action_result}, crate::DatabaseError> {{
|
) -> Result<{action_result}, crate::DatabaseError> {{
|
||||||
use crate::schema::{schema}::dsl::*;
|
use crate::schema::{schema}::dsl::*;
|
||||||
let msg = self;
|
let msg = self;
|
||||||
crate::q!({query}).first(conn).map_err(|e| {{
|
crate::q!({query}).first(conn).map_err(|e| {{
|
||||||
common::log::error!("{{:?}}", e);
|
::tracing::error!("{{:?}}", e);
|
||||||
crate::DatabaseError::GenericFailure(
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::OperationError::LoadSingle,
|
crate::OperationError::LoadSingle,
|
||||||
crate::ResourceKind::{resource},
|
crate::ResourceKind::{resource},
|
||||||
@ -221,12 +227,12 @@ fn build_load_exec(
|
|||||||
impl {name} {{
|
impl {name} {{
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
self,
|
self,
|
||||||
conn: &crate::DbPooledConn,
|
conn: &mut crate::DbPooledConn,
|
||||||
) -> Result<{action_result}, crate::DatabaseError> {{
|
) -> Result<{action_result}, crate::DatabaseError> {{
|
||||||
use crate::schema::{schema}::dsl::*;
|
use crate::schema::{schema}::dsl::*;
|
||||||
let msg = self;
|
let msg = self;
|
||||||
crate::q!({query}).load(conn).map_err(|e| {{
|
crate::q!({query}).load(conn).map_err(|e| {{
|
||||||
common::log::error!("{{:?}}", e);
|
::tracing::error!("{{:?}}", e);
|
||||||
crate::DatabaseError::GenericFailure(
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::OperationError::LoadCollection,
|
crate::OperationError::LoadCollection,
|
||||||
crate::ResourceKind::{resource},
|
crate::ResourceKind::{resource},
|
||||||
@ -255,12 +261,12 @@ fn build_update_exec(
|
|||||||
impl {name} {{
|
impl {name} {{
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
self,
|
self,
|
||||||
conn: &crate::DbPooledConn,
|
conn: &mut crate::DbPooledConn,
|
||||||
) -> Result<{action_result}, crate::DatabaseError> {{
|
) -> Result<{action_result}, crate::DatabaseError> {{
|
||||||
use crate::schema::{schema}::dsl::*;
|
use crate::schema::{schema}::dsl::*;
|
||||||
let msg = self;
|
let msg = self;
|
||||||
crate::q!({query}).get_result(conn).map_err(|e| {{
|
crate::q!({query}).get_result(conn).map_err(|e| {{
|
||||||
common::log::error!("{{:?}}", e);
|
::tracing::error!("{{:?}}", e);
|
||||||
crate::DatabaseError::GenericFailure(
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::OperationError::Update,
|
crate::OperationError::Update,
|
||||||
crate::ResourceKind::{resource},
|
crate::ResourceKind::{resource},
|
||||||
@ -289,12 +295,12 @@ fn build_destroy_exec(
|
|||||||
impl {name} {{
|
impl {name} {{
|
||||||
pub fn execute(
|
pub fn execute(
|
||||||
self,
|
self,
|
||||||
conn: &crate::DbPooledConn,
|
conn: &mut crate::DbPooledConn,
|
||||||
) -> Result<{action_result}, crate::DatabaseError> {{
|
) -> Result<{action_result}, crate::DatabaseError> {{
|
||||||
use crate::schema::{schema}::dsl::*;
|
use crate::schema::{schema}::dsl::*;
|
||||||
let msg = self;
|
let msg = self;
|
||||||
crate::q!({query}).execute(conn).map_err(|e| {{
|
crate::q!({query}).execute(conn).map_err(|e| {{
|
||||||
common::log::error!("{{:?}}", e);
|
::tracing::error!("{{:?}}", e);
|
||||||
crate::DatabaseError::GenericFailure(
|
crate::DatabaseError::GenericFailure(
|
||||||
crate::OperationError::Delete,
|
crate::OperationError::Delete,
|
||||||
crate::ResourceKind::{resource},
|
crate::ResourceKind::{resource},
|
@ -3,7 +3,7 @@ use std::iter::Peekable;
|
|||||||
use proc_macro::token_stream::IntoIter;
|
use proc_macro::token_stream::IntoIter;
|
||||||
use proc_macro::TokenTree;
|
use proc_macro::TokenTree;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Attributes {
|
pub struct Attributes {
|
||||||
pub result: Option<String>,
|
pub result: Option<String>,
|
||||||
pub schema: Option<String>,
|
pub schema: Option<String>,
|
9
crates/derive_db_execute/src/schema.rs
Normal file
9
crates/derive_db_execute/src/schema.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
|
||||||
|
issues (id) {
|
||||||
|
id -> Int4,
|
||||||
|
title -> Text,
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,8 @@ name = "derive_enum_iter"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
@ -27,7 +27,7 @@ fn consume_ident(mut it: Peekable<IntoIter>, name: &str) -> Peekable<IntoIter> {
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate) fn codegen(mut it: Peekable<IntoIter>) -> Result<String, String> {
|
pub(crate) fn codegen(mut it: Peekable<IntoIter>) -> Result<String, String> {
|
||||||
let name = it
|
let name = it
|
||||||
.next()
|
.next()
|
||||||
.expect("Expect to struct name but nothing was found");
|
.expect("Expect to struct name but nothing was found");
|
@ -3,8 +3,8 @@ name = "derive_enum_primitive"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
354
crates/derive_enum_primitive/src/lib.rs
Normal file
354
crates/derive_enum_primitive/src/lib.rs
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use proc_macro::{TokenStream, TokenTree};
|
||||||
|
|
||||||
|
fn as_str(name: &str, variants: &[String]) -> String {
|
||||||
|
let mut code = format!(
|
||||||
|
r#"
|
||||||
|
impl {name} {{
|
||||||
|
pub fn as_str(&self) -> &'static str {{
|
||||||
|
match self {{
|
||||||
|
"#,
|
||||||
|
name = name,
|
||||||
|
);
|
||||||
|
|
||||||
|
for variant in variants {
|
||||||
|
let lower = variant.to_lowercase();
|
||||||
|
code.push_str(
|
||||||
|
format!(
|
||||||
|
" {name}::{variant} => \"{lower}\",\n",
|
||||||
|
variant = variant,
|
||||||
|
name = name,
|
||||||
|
lower = lower
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
code.push_str(" }\n }\n}");
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_str(name: &str, variants: &[String]) -> String {
|
||||||
|
let mut code = format!(
|
||||||
|
r#"
|
||||||
|
impl std::str::FromStr for {name} {{
|
||||||
|
type Err = String;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {{
|
||||||
|
match s {{
|
||||||
|
"#,
|
||||||
|
name = name,
|
||||||
|
);
|
||||||
|
|
||||||
|
for variant in variants {
|
||||||
|
let lower = variant.to_lowercase();
|
||||||
|
code.push_str(
|
||||||
|
format!(
|
||||||
|
" \"{lower}\" => Ok({name}::{variant}),\n",
|
||||||
|
variant = variant,
|
||||||
|
name = name,
|
||||||
|
lower = lower
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
code.push_str(
|
||||||
|
format!(
|
||||||
|
" _ => Err(format!(\"Unknown {name} {{}}\", s)),",
|
||||||
|
name = name
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
code.push_str(" }\n }\n}");
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_label(name: &str, variants: &[String]) -> String {
|
||||||
|
let mut code = format!(
|
||||||
|
r#"
|
||||||
|
impl {name} {{
|
||||||
|
pub fn to_label(&self) -> &'static str {{
|
||||||
|
match self {{
|
||||||
|
"#,
|
||||||
|
name = name,
|
||||||
|
);
|
||||||
|
|
||||||
|
for variant in variants {
|
||||||
|
code.push_str(
|
||||||
|
format!(
|
||||||
|
" {name}::{variant} => \"{variant}\",\n",
|
||||||
|
variant = variant,
|
||||||
|
name = name,
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
code.push_str(" }\n }\n}");
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_u32(name: &str, variants: &[String]) -> String {
|
||||||
|
let mut code = format!(
|
||||||
|
r#"
|
||||||
|
impl Into<u32> for {name} {{
|
||||||
|
fn into(self) -> u32 {{
|
||||||
|
match self {{
|
||||||
|
"#,
|
||||||
|
name = name
|
||||||
|
);
|
||||||
|
for (idx, variant) in variants.iter().enumerate() {
|
||||||
|
code.push_str(
|
||||||
|
format!(
|
||||||
|
" {name}::{variant} => {idx},\n",
|
||||||
|
variant = variant,
|
||||||
|
name = name,
|
||||||
|
idx = idx
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
code.push_str(" }\n }\n}");
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_u32(name: &str, variants: &[String]) -> String {
|
||||||
|
let mut code = format!(
|
||||||
|
r#"
|
||||||
|
impl Into<{name}> for u32 {{
|
||||||
|
fn into(self) -> {name} {{
|
||||||
|
match self {{
|
||||||
|
"#,
|
||||||
|
name = name
|
||||||
|
);
|
||||||
|
for (idx, variant) in variants.iter().enumerate() {
|
||||||
|
code.push_str(
|
||||||
|
format!(
|
||||||
|
" {idx} => {name}::{variant},\n",
|
||||||
|
variant = variant,
|
||||||
|
name = name,
|
||||||
|
idx = idx
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
code.push_str(format!(" _ => {name}::default(),\n", name = name,).as_str());
|
||||||
|
code.push_str(" }\n }\n}");
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(EnumU32)]
|
||||||
|
pub fn derive_enum_u32(item: TokenStream) -> TokenStream {
|
||||||
|
let mut it = item.into_iter().peekable();
|
||||||
|
|
||||||
|
while let Some(token) = it.peek() {
|
||||||
|
match token {
|
||||||
|
TokenTree::Ident(_) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "pub" {
|
||||||
|
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword pub but nothing was found")
|
||||||
|
}
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "enum" {
|
||||||
|
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword struct but nothing was found")
|
||||||
|
}
|
||||||
|
let name = it
|
||||||
|
.next()
|
||||||
|
.expect("Expect to struct name but nothing was found")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut variants = vec![];
|
||||||
|
if let Some(TokenTree::Group(group)) = it.next() {
|
||||||
|
for token in group.stream() {
|
||||||
|
if let TokenTree::Ident(ident) = token {
|
||||||
|
variants.push(ident.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Enum variants group expected");
|
||||||
|
}
|
||||||
|
if variants.is_empty() {
|
||||||
|
panic!("Enum cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut code = String::new();
|
||||||
|
code.push_str(into_u32(&name, &variants).as_str());
|
||||||
|
code.push_str(from_u32(&name, &variants).as_str());
|
||||||
|
code.parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(EnumLabel)]
|
||||||
|
pub fn derive_enum_label(item: TokenStream) -> TokenStream {
|
||||||
|
let mut it = item.into_iter().peekable();
|
||||||
|
|
||||||
|
while let Some(token) = it.peek() {
|
||||||
|
match token {
|
||||||
|
TokenTree::Ident(_) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "pub" {
|
||||||
|
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword pub but nothing was found")
|
||||||
|
}
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "enum" {
|
||||||
|
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword struct but nothing was found")
|
||||||
|
}
|
||||||
|
let name = it
|
||||||
|
.next()
|
||||||
|
.expect("Expect to struct name but nothing was found")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut variants = vec![];
|
||||||
|
if let Some(TokenTree::Group(group)) = it.next() {
|
||||||
|
for token in group.stream() {
|
||||||
|
if let TokenTree::Ident(ident) = token {
|
||||||
|
variants.push(ident.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Enum variants group expected");
|
||||||
|
}
|
||||||
|
if variants.is_empty() {
|
||||||
|
panic!("Enum cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut code = String::new();
|
||||||
|
code.push_str(into_label(&name, &variants).as_str());
|
||||||
|
code.parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(EnumStr)]
|
||||||
|
pub fn derive_enum_str(item: TokenStream) -> TokenStream {
|
||||||
|
let mut it = item.into_iter().peekable();
|
||||||
|
|
||||||
|
while let Some(token) = it.peek() {
|
||||||
|
match token {
|
||||||
|
TokenTree::Ident(_) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "pub" {
|
||||||
|
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword pub but nothing was found")
|
||||||
|
}
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "enum" {
|
||||||
|
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword struct but nothing was found")
|
||||||
|
}
|
||||||
|
let name = it
|
||||||
|
.next()
|
||||||
|
.expect("Expect to struct name but nothing was found")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut variants = vec![];
|
||||||
|
if let Some(TokenTree::Group(group)) = it.next() {
|
||||||
|
for token in group.stream() {
|
||||||
|
if let TokenTree::Ident(ident) = token {
|
||||||
|
variants.push(ident.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Enum variants group expected");
|
||||||
|
}
|
||||||
|
if variants.is_empty() {
|
||||||
|
panic!("Enum cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut code = String::new();
|
||||||
|
|
||||||
|
code.push_str(from_str(&name, &variants).as_str());
|
||||||
|
|
||||||
|
code.parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(EnumAsStr)]
|
||||||
|
pub fn derive_enum_as_str(item: TokenStream) -> TokenStream {
|
||||||
|
let mut it = item.into_iter().peekable();
|
||||||
|
|
||||||
|
while let Some(token) = it.peek() {
|
||||||
|
match token {
|
||||||
|
TokenTree::Ident(_) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "pub" {
|
||||||
|
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword pub but nothing was found")
|
||||||
|
}
|
||||||
|
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||||
|
if ident.to_string().as_str() != "enum" {
|
||||||
|
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expect to find keyword struct but nothing was found")
|
||||||
|
}
|
||||||
|
let name = it
|
||||||
|
.next()
|
||||||
|
.expect("Expect to struct name but nothing was found")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut variants = vec![];
|
||||||
|
if let Some(TokenTree::Group(group)) = it.next() {
|
||||||
|
for token in group.stream() {
|
||||||
|
if let TokenTree::Ident(ident) = token {
|
||||||
|
variants.push(ident.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Enum variants group expected");
|
||||||
|
}
|
||||||
|
if variants.is_empty() {
|
||||||
|
panic!("Enum cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut code = String::new();
|
||||||
|
|
||||||
|
code.push_str(as_str(&name, &variants).as_str());
|
||||||
|
|
||||||
|
code.parse().unwrap()
|
||||||
|
}
|
26
crates/derive_enum_sql/Cargo.toml
Normal file
26
crates/derive_enum_sql/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "diesel-derive-enum"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "Derive diesel boilerplate for using enums in databases"
|
||||||
|
authors = ["Alex Whitney <adwhit@fastmail.com>"]
|
||||||
|
repository = "http://github.com/adwhit/diesel-derive-enum"
|
||||||
|
homepage = "http://github.com/adwhit/diesel-derive-enum"
|
||||||
|
keywords = ["diesel", "postgres", "sqlite", "mysql", "sql"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
readme = "README.md"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1"
|
||||||
|
syn = "1"
|
||||||
|
heck = "0.4.0"
|
||||||
|
proc-macro2 = "1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
postgres = []
|
||||||
|
sqlite = []
|
||||||
|
mysql = []
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "diesel_derive_enum"
|
||||||
|
proc-macro = true
|
500
crates/derive_enum_sql/src/lib.rs
Normal file
500
crates/derive_enum_sql/src/lib.rs
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
#![recursion_limit = "1024"]
|
||||||
|
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::{Ident, Span};
|
||||||
|
use quote::quote;
|
||||||
|
use syn::*;
|
||||||
|
|
||||||
|
/// Implement the traits necessary for inserting the enum directly into a
|
||||||
|
/// database
|
||||||
|
///
|
||||||
|
/// # Attributes
|
||||||
|
///
|
||||||
|
/// ## Type attributes
|
||||||
|
///
|
||||||
|
/// * `#[ExistingTypePath = "crate::schema::sql_types::NewEnum"]` specifies the
|
||||||
|
/// path to a corresponding diesel type that was already created by the diesel
|
||||||
|
/// CLI. If omitted, the type will be generated by this macro. *Note*: Only
|
||||||
|
/// applies to `postgres`, will error if specified for other databases
|
||||||
|
/// * `#[DieselType = "NewEnumMapping"]` specifies the name for the diesel type
|
||||||
|
/// to create. If omitted, uses `<enum name>Mapping`. *Note*: Cannot be
|
||||||
|
/// specified alongside `ExistingTypePath`
|
||||||
|
/// * `#[DbValueStyle = "snake_case"]` specifies a renaming style from each of
|
||||||
|
/// the rust enum variants to each of the database variants. Either
|
||||||
|
/// `camelCase`, `kebab-case`, `PascalCase`, `SCREAMING_SNAKE_CASE`,
|
||||||
|
/// `snake_case`, `verbatim`. If omitted, uses `snake_case`.
|
||||||
|
///
|
||||||
|
/// ## Variant attributes
|
||||||
|
///
|
||||||
|
/// * `#[db_rename = "variant"]` specifies the db name for a specific variant.
|
||||||
|
#[proc_macro_derive(
|
||||||
|
DbEnum,
|
||||||
|
attributes(PgType, DieselType, ExistingTypePath, DbValueStyle, db_rename)
|
||||||
|
)]
|
||||||
|
pub fn derive(input: TokenStream) -> TokenStream {
|
||||||
|
let input: DeriveInput = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
let existing_mapping_path = val_from_attrs(&input.attrs, "ExistingTypePath");
|
||||||
|
if !cfg!(feature = "postgres") && existing_mapping_path.is_some() {
|
||||||
|
panic!("ExistingTypePath attribute only applies when the 'postgres' feature is enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// we could allow a default value here but... I'm not very keen
|
||||||
|
// let existing_mapping_path = existing_mapping_path
|
||||||
|
// .unwrap_or_else(|| format!("crate::schema::sql_types::{}", input.ident));
|
||||||
|
|
||||||
|
let pg_internal_type = val_from_attrs(&input.attrs, "PgType");
|
||||||
|
|
||||||
|
if existing_mapping_path.is_some() && pg_internal_type.is_some() {
|
||||||
|
panic!("Cannot specify both `ExistingTypePath` and `PgType` attributes");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pg_internal_type = pg_internal_type.unwrap_or(input.ident.to_string().to_snake_case());
|
||||||
|
|
||||||
|
let new_diesel_mapping = val_from_attrs(&input.attrs, "DieselType");
|
||||||
|
if existing_mapping_path.is_some() && new_diesel_mapping.is_some() {
|
||||||
|
panic!("Cannot specify both `ExistingTypePath` and `DieselType` attributes");
|
||||||
|
}
|
||||||
|
let new_diesel_mapping = new_diesel_mapping.unwrap_or_else(|| format!("{}Type", input.ident));
|
||||||
|
|
||||||
|
// Maintain backwards compatibility by defaulting to snake case.
|
||||||
|
let case_style =
|
||||||
|
val_from_attrs(&input.attrs, "DbValueStyle").unwrap_or_else(|| "snake_case".to_string());
|
||||||
|
let case_style = CaseStyle::from_string(&case_style);
|
||||||
|
|
||||||
|
let existing_mapping_path = existing_mapping_path.map(|v| {
|
||||||
|
v.parse::<proc_macro2::TokenStream>()
|
||||||
|
.expect("ExistingTypePath is not a valid token")
|
||||||
|
});
|
||||||
|
let new_diesel_mapping = Ident::new(new_diesel_mapping.as_ref(), Span::call_site());
|
||||||
|
if let Data::Enum(syn::DataEnum {
|
||||||
|
variants: data_variants,
|
||||||
|
..
|
||||||
|
}) = input.data
|
||||||
|
{
|
||||||
|
generate_derive_enum_impls(
|
||||||
|
&existing_mapping_path,
|
||||||
|
&new_diesel_mapping,
|
||||||
|
&pg_internal_type,
|
||||||
|
case_style,
|
||||||
|
&input.ident,
|
||||||
|
&data_variants,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"derive(DbEnum) can only be applied to enums",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn val_from_attrs(attrs: &[Attribute], attr_name: &str) -> Option<String> {
|
||||||
|
for attr in attrs {
|
||||||
|
if attr.path.is_ident(attr_name) {
|
||||||
|
match attr.parse_meta().ok()? {
|
||||||
|
Meta::NameValue(MetaNameValue {
|
||||||
|
lit: Lit::Str(lit_str),
|
||||||
|
..
|
||||||
|
}) => return Some(lit_str.value()),
|
||||||
|
_ => panic!(
|
||||||
|
"Attribute '{}' must have form: {} = \"value\"",
|
||||||
|
attr_name, attr_name
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines the casing for the database representation. Follows serde naming
|
||||||
|
/// convention.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum CaseStyle {
|
||||||
|
Camel,
|
||||||
|
Kebab,
|
||||||
|
Pascal,
|
||||||
|
Upper,
|
||||||
|
ScreamingSnake,
|
||||||
|
Snake,
|
||||||
|
Verbatim,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CaseStyle {
|
||||||
|
fn from_string(name: &str) -> Self {
|
||||||
|
match name {
|
||||||
|
"camelCase" => CaseStyle::Camel,
|
||||||
|
"kebab-case" => CaseStyle::Kebab,
|
||||||
|
"PascalCase" => CaseStyle::Pascal,
|
||||||
|
"SCREAMING_SNAKE_CASE" => CaseStyle::ScreamingSnake,
|
||||||
|
"UPPERCASE" => CaseStyle::Upper,
|
||||||
|
"snake_case" => CaseStyle::Snake,
|
||||||
|
"verbatim" | "verbatimcase" => CaseStyle::Verbatim,
|
||||||
|
s => panic!("unsupported casing: `{}`", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_derive_enum_impls(
|
||||||
|
existing_mapping_path: &Option<proc_macro2::TokenStream>,
|
||||||
|
new_diesel_mapping: &Ident,
|
||||||
|
pg_internal_type: &str,
|
||||||
|
case_style: CaseStyle,
|
||||||
|
enum_ty: &Ident,
|
||||||
|
variants: &syn::punctuated::Punctuated<Variant, syn::token::Comma>,
|
||||||
|
) -> TokenStream {
|
||||||
|
let modname = Ident::new(&format!("db_enum_impl_{}", enum_ty), Span::call_site());
|
||||||
|
let variant_ids: Vec<proc_macro2::TokenStream> = variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| {
|
||||||
|
if let Fields::Unit = variant.fields {
|
||||||
|
let id = &variant.ident;
|
||||||
|
quote! {
|
||||||
|
#enum_ty::#id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Variants must be fieldless")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let variants_db: Vec<String> = variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| {
|
||||||
|
val_from_attrs(&variant.attrs, "db_rename")
|
||||||
|
.unwrap_or_else(|| stylize_value(&variant.ident.to_string(), case_style))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let variants_db_bytes: Vec<LitByteStr> = variants_db
|
||||||
|
.iter()
|
||||||
|
.map(|variant_str| LitByteStr::new(variant_str.as_bytes(), Span::call_site()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let common = generate_common(enum_ty, &variant_ids, &variants_db, &variants_db_bytes);
|
||||||
|
let (diesel_mapping_def, diesel_mapping_use) =
|
||||||
|
// Skip this part if we already have an existing mapping
|
||||||
|
if existing_mapping_path.is_some() {
|
||||||
|
(None, None)
|
||||||
|
} else {
|
||||||
|
let new_diesel_mapping_def = generate_new_diesel_mapping(new_diesel_mapping, pg_internal_type);
|
||||||
|
let common_impls_on_new_diesel_mapping =
|
||||||
|
generate_common_impls("e! { #new_diesel_mapping }, enum_ty);
|
||||||
|
(
|
||||||
|
Some(quote! {
|
||||||
|
#new_diesel_mapping_def
|
||||||
|
#common_impls_on_new_diesel_mapping
|
||||||
|
}),
|
||||||
|
Some(quote! {
|
||||||
|
pub use self::#modname::#new_diesel_mapping;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let pg_impl = if cfg!(feature = "postgres") {
|
||||||
|
match existing_mapping_path {
|
||||||
|
Some(path) => {
|
||||||
|
let common_impls_on_existing_diesel_mapping = generate_common_impls(path, enum_ty);
|
||||||
|
let postgres_impl = generate_postgres_impl(path, enum_ty, true);
|
||||||
|
Some(quote! {
|
||||||
|
#common_impls_on_existing_diesel_mapping
|
||||||
|
#postgres_impl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Some(generate_postgres_impl(
|
||||||
|
"e! { #new_diesel_mapping },
|
||||||
|
enum_ty,
|
||||||
|
false,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mysql_impl = if cfg!(feature = "mysql") {
|
||||||
|
Some(generate_mysql_impl(new_diesel_mapping, enum_ty))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let sqlite_impl = if cfg!(feature = "sqlite") {
|
||||||
|
Some(generate_sqlite_impl(new_diesel_mapping, enum_ty))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let imports = quote! {
|
||||||
|
use super::*;
|
||||||
|
use diesel::{
|
||||||
|
backend::{self, Backend},
|
||||||
|
deserialize::{self, FromSql},
|
||||||
|
expression::AsExpression,
|
||||||
|
internal::derives::as_expression::Bound,
|
||||||
|
query_builder::{bind_collector::RawBytesBindCollector, QueryId},
|
||||||
|
row::Row,
|
||||||
|
serialize::{self, IsNull, Output, ToSql},
|
||||||
|
sql_types::*,
|
||||||
|
Queryable,
|
||||||
|
};
|
||||||
|
use std::io::Write;
|
||||||
|
};
|
||||||
|
|
||||||
|
let quoted = quote! {
|
||||||
|
#diesel_mapping_use
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod #modname {
|
||||||
|
#imports
|
||||||
|
|
||||||
|
#common
|
||||||
|
#diesel_mapping_def
|
||||||
|
#pg_impl
|
||||||
|
#mysql_impl
|
||||||
|
#sqlite_impl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quoted.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stylize_value(value: &str, style: CaseStyle) -> String {
|
||||||
|
match style {
|
||||||
|
CaseStyle::Camel => value.to_lower_camel_case(),
|
||||||
|
CaseStyle::Kebab => value.to_kebab_case(),
|
||||||
|
CaseStyle::Pascal => value.to_upper_camel_case(),
|
||||||
|
CaseStyle::Upper => value.to_uppercase(),
|
||||||
|
CaseStyle::ScreamingSnake => value.to_shouty_snake_case(),
|
||||||
|
CaseStyle::Snake => value.to_snake_case(),
|
||||||
|
CaseStyle::Verbatim => value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_common(
|
||||||
|
enum_ty: &Ident,
|
||||||
|
variants_rs: &[proc_macro2::TokenStream],
|
||||||
|
variants_db: &[String],
|
||||||
|
variants_db_bytes: &[LitByteStr],
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
quote! {
|
||||||
|
fn db_str_representation(e: &#enum_ty) -> &'static str {
|
||||||
|
match *e {
|
||||||
|
#(#variants_rs => #variants_db,)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_db_binary_representation(bytes: &[u8]) -> deserialize::Result<#enum_ty> {
|
||||||
|
match bytes {
|
||||||
|
#(#variants_db_bytes => Ok(#variants_rs),)*
|
||||||
|
v => Err(format!("Unrecognized enum variant: '{}'",
|
||||||
|
String::from_utf8_lossy(v)).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_new_diesel_mapping(
|
||||||
|
new_diesel_mapping: &Ident,
|
||||||
|
pg_internal_type: &str,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
// Note - we only generate a new mapping for mysql and sqlite, postgres
|
||||||
|
// should already have one
|
||||||
|
quote! {
|
||||||
|
#[derive(SqlType, Clone)]
|
||||||
|
#[diesel(mysql_type(name = "Enum"))]
|
||||||
|
#[diesel(sqlite_type(name = "Text"))]
|
||||||
|
#[diesel(postgres_type(name = #pg_internal_type))]
|
||||||
|
pub struct #new_diesel_mapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_common_impls(
|
||||||
|
diesel_mapping: &proc_macro2::TokenStream,
|
||||||
|
enum_ty: &Ident,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
quote! {
|
||||||
|
|
||||||
|
// NOTE: at some point this impl will no longer be necessary
|
||||||
|
// for diesel-cli schemas
|
||||||
|
// See https://github.com/adwhit/diesel-derive-enum/issues/10
|
||||||
|
// and https://github.com/adwhit/diesel-derive-enum/pull/79
|
||||||
|
impl QueryId for #diesel_mapping {
|
||||||
|
type QueryId = #diesel_mapping;
|
||||||
|
const HAS_STATIC_QUERY_ID: bool = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsExpression<#diesel_mapping> for #enum_ty {
|
||||||
|
type Expression = Bound<#diesel_mapping, Self>;
|
||||||
|
|
||||||
|
fn as_expression(self) -> Self::Expression {
|
||||||
|
Bound::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsExpression<Nullable<#diesel_mapping>> for #enum_ty {
|
||||||
|
type Expression = Bound<Nullable<#diesel_mapping>, Self>;
|
||||||
|
|
||||||
|
fn as_expression(self) -> Self::Expression {
|
||||||
|
Bound::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsExpression<#diesel_mapping> for &'a #enum_ty {
|
||||||
|
type Expression = Bound<#diesel_mapping, Self>;
|
||||||
|
|
||||||
|
fn as_expression(self) -> Self::Expression {
|
||||||
|
Bound::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsExpression<Nullable<#diesel_mapping>> for &'a #enum_ty {
|
||||||
|
type Expression = Bound<Nullable<#diesel_mapping>, Self>;
|
||||||
|
|
||||||
|
fn as_expression(self) -> Self::Expression {
|
||||||
|
Bound::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> AsExpression<#diesel_mapping> for &'a &'b #enum_ty {
|
||||||
|
type Expression = Bound<#diesel_mapping, Self>;
|
||||||
|
|
||||||
|
fn as_expression(self) -> Self::Expression {
|
||||||
|
Bound::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> AsExpression<Nullable<#diesel_mapping>> for &'a &'b #enum_ty {
|
||||||
|
type Expression = Bound<Nullable<#diesel_mapping>, Self>;
|
||||||
|
|
||||||
|
fn as_expression(self) -> Self::Expression {
|
||||||
|
Bound::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DB> ToSql<Nullable<#diesel_mapping>, DB> for #enum_ty
|
||||||
|
where
|
||||||
|
DB: Backend,
|
||||||
|
Self: ToSql<#diesel_mapping, DB>,
|
||||||
|
{
|
||||||
|
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
|
||||||
|
ToSql::<#diesel_mapping, DB>::to_sql(self, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_postgres_impl(
|
||||||
|
diesel_mapping: &proc_macro2::TokenStream,
|
||||||
|
enum_ty: &Ident,
|
||||||
|
with_clone: bool,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
// If the type was generated by postgres, we have to manually add a clone impl,
|
||||||
|
// if generated by 'us' it has already been done
|
||||||
|
let clone_impl = if with_clone {
|
||||||
|
Some(quote! {
|
||||||
|
impl Clone for #diesel_mapping {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
#diesel_mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
mod pg_impl {
|
||||||
|
use super::*;
|
||||||
|
use diesel::pg::{Pg, PgValue};
|
||||||
|
|
||||||
|
#clone_impl
|
||||||
|
|
||||||
|
impl FromSql<#diesel_mapping, Pg> for #enum_ty {
|
||||||
|
fn from_sql(raw: PgValue) -> deserialize::Result<Self> {
|
||||||
|
from_db_binary_representation(raw.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql<#diesel_mapping, Pg> for #enum_ty
|
||||||
|
{
|
||||||
|
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
|
||||||
|
out.write_all(db_str_representation(self).as_bytes())?;
|
||||||
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queryable<#diesel_mapping, Pg> for #enum_ty {
|
||||||
|
type Row = Self;
|
||||||
|
|
||||||
|
fn build(row: Self::Row) -> deserialize::Result<Self> {
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_mysql_impl(diesel_mapping: &Ident, enum_ty: &Ident) -> proc_macro2::TokenStream {
|
||||||
|
quote! {
|
||||||
|
mod mysql_impl {
|
||||||
|
use super::*;
|
||||||
|
use diesel;
|
||||||
|
use diesel::mysql::{Mysql, MysqlValue};
|
||||||
|
|
||||||
|
impl FromSql<#diesel_mapping, Mysql> for #enum_ty {
|
||||||
|
fn from_sql(raw: MysqlValue) -> deserialize::Result<Self> {
|
||||||
|
from_db_binary_representation(raw.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql<#diesel_mapping, Mysql> for #enum_ty
|
||||||
|
{
|
||||||
|
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
|
||||||
|
out.write_all(db_str_representation(self).as_bytes())?;
|
||||||
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queryable<#diesel_mapping, Mysql> for #enum_ty {
|
||||||
|
type Row = Self;
|
||||||
|
|
||||||
|
fn build(row: Self::Row) -> deserialize::Result<Self> {
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_sqlite_impl(diesel_mapping: &Ident, enum_ty: &Ident) -> proc_macro2::TokenStream {
|
||||||
|
quote! {
|
||||||
|
mod sqlite_impl {
|
||||||
|
use super::*;
|
||||||
|
use diesel;
|
||||||
|
use diesel::sql_types;
|
||||||
|
use diesel::sqlite::Sqlite;
|
||||||
|
|
||||||
|
impl FromSql<#diesel_mapping, Sqlite> for #enum_ty {
|
||||||
|
fn from_sql(value: backend::RawValue<Sqlite>) -> deserialize::Result<Self> {
|
||||||
|
let bytes = <Vec<u8> as FromSql<sql_types::Binary, Sqlite>>::from_sql(value)?;
|
||||||
|
from_db_binary_representation(bytes.as_slice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql<#diesel_mapping, Sqlite> for #enum_ty {
|
||||||
|
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
|
||||||
|
<str as ToSql<sql_types::Text, Sqlite>>::to_sql(db_str_representation(self), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queryable<#diesel_mapping, Sqlite> for #enum_ty {
|
||||||
|
type Row = Self;
|
||||||
|
|
||||||
|
fn build(row: Self::Row) -> deserialize::Result<Self> {
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,8 @@ name = "derive_utils"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
21
crates/filesystem-actor/Cargo.toml
Normal file
21
crates/filesystem-actor/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "filesystem-actor"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "filesystem_actor"
|
||||||
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0.13.0" }
|
||||||
|
actix-files = { version = "0.6.2" }
|
||||||
|
bitque-config = { workspace = true, features = ["local-storage"] }
|
||||||
|
bytes = { version = "1.4.0" }
|
||||||
|
futures = { version = "0.3.8" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use actix::SyncContext;
|
use actix::SyncContext;
|
||||||
use actix_files::{self, Files};
|
use actix_files::{self, Files};
|
||||||
use jirs_config::fs::Configuration;
|
use bitque_config::fs::Configuration;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FsError {
|
pub enum FsError {
|
||||||
@ -13,11 +13,11 @@ pub enum FsError {
|
|||||||
WriteFile,
|
WriteFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileSystemExecutor {
|
pub struct LocalStorageExecutor {
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemExecutor {
|
impl LocalStorageExecutor {
|
||||||
pub fn client_path(&self) -> &str {
|
pub fn client_path(&self) -> &str {
|
||||||
self.config.client_path.as_str()
|
self.config.client_path.as_str()
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ impl FileSystemExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FileSystemExecutor {
|
impl Default for LocalStorageExecutor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
config: Configuration::read(),
|
config: Configuration::read(),
|
||||||
@ -35,18 +35,18 @@ impl Default for FileSystemExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix::Actor for FileSystemExecutor {
|
impl actix::Actor for LocalStorageExecutor {
|
||||||
type Context = SyncContext<Self>;
|
type Context = SyncContext<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(actix::Message)]
|
#[derive(actix::Message)]
|
||||||
#[rtype(result = "Result<usize, FsError>")]
|
#[rtype(result = "Result<usize, FsError>")]
|
||||||
pub struct CreateFile {
|
pub struct CreateFile {
|
||||||
pub source: tokio::sync::broadcast::Receiver<common::bytes::Bytes>,
|
pub source: tokio::sync::broadcast::Receiver<bytes::Bytes>,
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix::Handler<CreateFile> for FileSystemExecutor {
|
impl actix::Handler<CreateFile> for LocalStorageExecutor {
|
||||||
type Result = Result<usize, FsError>;
|
type Result = Result<usize, FsError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: CreateFile, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: CreateFile, _ctx: &mut Self::Context) -> Self::Result {
|
@ -3,8 +3,8 @@ name = "highlight-actor"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
@ -13,23 +13,13 @@ name = "highlight_actor"
|
|||||||
path = "./src/lib.rs"
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../../shared/common" }
|
actix = { version = "0.13.0" }
|
||||||
actix = { version = "0.10.0" }
|
bincode = { version = "*" }
|
||||||
|
bitque-config = { workspace = true, features = ["hi"] }
|
||||||
serde = "*"
|
bitque-data = { workspace = true, features = ["backend"] }
|
||||||
bincode = "*"
|
|
||||||
toml = { version = "*" }
|
|
||||||
|
|
||||||
simsearch = { version = "0.2" }
|
|
||||||
|
|
||||||
flate2 = { version = "*" }
|
flate2 = { version = "*" }
|
||||||
syntect = { version = "*" }
|
|
||||||
lazy_static = { version = "*" }
|
lazy_static = { version = "*" }
|
||||||
|
serde = { version = "*" }
|
||||||
[dependencies.jirs-config]
|
simsearch = { version = "0.2" }
|
||||||
path = "../../shared/jirs-config"
|
syntect = { version = "*" }
|
||||||
features = ["hi"]
|
toml = { version = "*" }
|
||||||
|
|
||||||
[dependencies.jirs-data]
|
|
||||||
path = "../../shared/jirs-data"
|
|
||||||
features = ["backend"]
|
|
1725
crates/highlight-actor/assets/InspiredGitHub.tmTheme
Normal file
1725
crates/highlight-actor/assets/InspiredGitHub.tmTheme
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix::{Actor, Handler, SyncContext};
|
use actix::{Actor, Handler, SyncContext};
|
||||||
use jirs_data::HighlightedCode;
|
use bitque_data::HighlightedCode;
|
||||||
use simsearch::SimSearch;
|
use simsearch::SimSearch;
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::highlighting::{Style, ThemeSet};
|
use syntect::highlighting::{Style, ThemeSet};
|
||||||
@ -75,11 +75,20 @@ impl HighlightActor {
|
|||||||
.theme_set
|
.theme_set
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.themes
|
.themes
|
||||||
.get("GitHub")
|
.get("InspiredGitHub")
|
||||||
.ok_or(HighlightError::UnknownTheme)?;
|
.ok_or(HighlightError::UnknownTheme)?;
|
||||||
|
|
||||||
let mut hi = HighlightLines::new(set, theme);
|
let mut hi = HighlightLines::new(set, theme);
|
||||||
Ok(hi.highlight(code, self.syntax_set.as_ref()))
|
|
||||||
|
let mut res = Vec::with_capacity(code.split_ascii_whitespace().count());
|
||||||
|
for line in code.lines() {
|
||||||
|
res.extend(
|
||||||
|
hi.highlight_line(line, self.syntax_set.as_ref())
|
||||||
|
.map_err(|_e| HighlightError::UnknownLanguage)?
|
||||||
|
.iter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,14 +114,14 @@ impl Handler<HighlightCode> for HighlightActor {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(style, part)| {
|
.map(|(style, part)| {
|
||||||
(
|
(
|
||||||
jirs_data::Style {
|
bitque_data::Style {
|
||||||
foreground: jirs_data::Color {
|
foreground: bitque_data::Color {
|
||||||
r: style.foreground.r,
|
r: style.foreground.r,
|
||||||
g: style.foreground.g,
|
g: style.foreground.g,
|
||||||
b: style.foreground.b,
|
b: style.foreground.b,
|
||||||
a: style.foreground.a,
|
a: style.foreground.a,
|
||||||
},
|
},
|
||||||
background: jirs_data::Color {
|
background: bitque_data::Color {
|
||||||
r: style.background.r,
|
r: style.background.r,
|
||||||
g: style.background.g,
|
g: style.background.g,
|
||||||
b: style.background.b,
|
b: style.background.b,
|
||||||
@ -128,7 +137,7 @@ impl Handler<HighlightCode> for HighlightActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(actix::Message, Default)]
|
#[derive(Default, actix::Message)]
|
||||||
#[rtype(result = "Result<String, HighlightError>")]
|
#[rtype(result = "Result<String, HighlightError>")]
|
||||||
pub struct TextHighlightCode {
|
pub struct TextHighlightCode {
|
||||||
pub code: String,
|
pub code: String,
|
12
crates/highlight-actor/src/load.rs
Normal file
12
crates/highlight-actor/src/load.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use syntect::highlighting::ThemeSet;
|
||||||
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn integrated_syntaxset() -> syntect::parsing::SyntaxSet {
|
||||||
|
SyntaxSet::load_defaults_newlines()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn integrated_themeset() -> syntect::highlighting::ThemeSet {
|
||||||
|
ThemeSet::load_defaults()
|
||||||
|
}
|
@ -3,8 +3,8 @@ name = "mail-actor"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
@ -13,25 +13,15 @@ name = "mail_actor"
|
|||||||
path = "./src/lib.rs"
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../../shared/common" }
|
actix = { version = "0.13.0" }
|
||||||
actix = { version = "0.10.0" }
|
bitque-config = { workspace = true, features = ["mail", "web"] }
|
||||||
|
|
||||||
serde = "*"
|
|
||||||
toml = { version = "*" }
|
|
||||||
|
|
||||||
dotenv = { version = "*" }
|
dotenv = { version = "*" }
|
||||||
|
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4", "v5"] }
|
|
||||||
|
|
||||||
futures = { version = "*" }
|
futures = { version = "*" }
|
||||||
openssl-sys = { version = "*", features = ["vendored"] }
|
|
||||||
libc = { version = "0.2.0", default-features = false }
|
|
||||||
|
|
||||||
lettre = { version = "0.10.0-rc.3" }
|
lettre = { version = "0.10.0-rc.3" }
|
||||||
lettre_email = { version = "*" }
|
lettre_email = { version = "*" }
|
||||||
|
libc = { version = "0.2.0", default-features = false }
|
||||||
log = { version = "*" }
|
openssl-sys = { version = "*", features = ["vendored"] }
|
||||||
|
serde = { version = "*" }
|
||||||
[dependencies.jirs-config]
|
toml = { version = "*" }
|
||||||
path = "../../shared/jirs-config"
|
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
|
||||||
features = ["mail", "web"]
|
tracing = { version = "0" }
|
@ -22,7 +22,7 @@ impl Handler<Invite> for MailExecutor {
|
|||||||
fn handle(&mut self, msg: Invite, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: Invite, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
use lettre::Transport;
|
use lettre::Transport;
|
||||||
let transport = &mut self.transport;
|
let transport = &mut self.transport;
|
||||||
let addr = jirs_config::web::Configuration::read().full_addr();
|
let addr = bitque_config::web::Configuration::read().full_addr();
|
||||||
let from = email_address(self.config.from.as_str())?;
|
let from = email_address(self.config.from.as_str())?;
|
||||||
let to = email_address(&msg.email)?;
|
let to = email_address(&msg.email)?;
|
||||||
|
|
||||||
@ -49,16 +49,16 @@ impl Handler<Invite> for MailExecutor {
|
|||||||
let mail = lettre::Message::builder()
|
let mail = lettre::Message::builder()
|
||||||
.to(Mailbox::new(None, to))
|
.to(Mailbox::new(None, to))
|
||||||
.from(Mailbox::new(None, from))
|
.from(Mailbox::new(None, from))
|
||||||
.subject("Invitation to JIRS project")
|
.subject("Invitation to BITQUE project")
|
||||||
.header(ContentType::TEXT_HTML)
|
.header(ContentType::TEXT_HTML)
|
||||||
.body(html)
|
.body(html)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("{:?}", e);
|
tracing::error!("{:?}", e);
|
||||||
MailError::MailformedBody
|
MailError::MalformedBody
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
transport.send(&mail).map(|_| ()).map_err(|e| {
|
transport.send(&mail).map(|_| ()).map_err(|e| {
|
||||||
log::error!("Mailer: {}", e);
|
tracing::error!("Mailer: {}", e);
|
||||||
MailError::FailedToSendEmail
|
MailError::FailedToSendEmail
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -15,12 +15,12 @@ pub enum MailError {
|
|||||||
EmailWithoutDomain,
|
EmailWithoutDomain,
|
||||||
InvalidEmailAddress,
|
InvalidEmailAddress,
|
||||||
FailedToSendEmail,
|
FailedToSendEmail,
|
||||||
MailformedBody,
|
MalformedBody,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MailExecutor {
|
pub struct MailExecutor {
|
||||||
pub transport: MailTransport,
|
pub transport: MailTransport,
|
||||||
pub config: jirs_config::mail::Configuration,
|
pub config: bitque_config::mail::Configuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor for MailExecutor {
|
impl Actor for MailExecutor {
|
||||||
@ -29,7 +29,7 @@ impl Actor for MailExecutor {
|
|||||||
|
|
||||||
impl Default for MailExecutor {
|
impl Default for MailExecutor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let config = jirs_config::mail::Configuration::read();
|
let config = bitque_config::mail::Configuration::read();
|
||||||
Self {
|
Self {
|
||||||
transport: mail_transport(&config),
|
transport: mail_transport(&config),
|
||||||
config,
|
config,
|
||||||
@ -37,8 +37,8 @@ impl Default for MailExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mail_client(config: &jirs_config::mail::Configuration) -> lettre::SmtpTransport {
|
fn mail_client(config: &bitque_config::mail::Configuration) -> lettre::SmtpTransport {
|
||||||
let jirs_config::mail::Configuration {
|
let bitque_config::mail::Configuration {
|
||||||
user: mail_user,
|
user: mail_user,
|
||||||
pass: mail_pass,
|
pass: mail_pass,
|
||||||
host: mail_host,
|
host: mail_host,
|
||||||
@ -52,7 +52,7 @@ fn mail_client(config: &jirs_config::mail::Configuration) -> lettre::SmtpTranspo
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mail_transport(config: &jirs_config::mail::Configuration) -> MailTransport {
|
fn mail_transport(config: &bitque_config::mail::Configuration) -> MailTransport {
|
||||||
mail_client(config)
|
mail_client(config)
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ impl Handler<Welcome> for MailExecutor {
|
|||||||
<html>
|
<html>
|
||||||
<head><meta charset="UTF-8"></head>
|
<head><meta charset="UTF-8"></head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Welcome in JIRS!</h1>
|
<h1>Welcome in BITQUE!</h1>
|
||||||
<p>
|
<p>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@ -45,22 +45,22 @@ impl Handler<Welcome> for MailExecutor {
|
|||||||
bind_token = msg.bind_token,
|
bind_token = msg.bind_token,
|
||||||
);
|
);
|
||||||
if cfg!(debug_assetrions) {
|
if cfg!(debug_assetrions) {
|
||||||
log::info!("Sending email:\n{}", html);
|
tracing::info!("Sending email:\n{}", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mail = lettre::Message::builder()
|
let mail = lettre::Message::builder()
|
||||||
.to(Mailbox::new(None, to))
|
.to(Mailbox::new(None, to))
|
||||||
.from(Mailbox::new(None, from))
|
.from(Mailbox::new(None, from))
|
||||||
.subject("Welcome to JIRS")
|
.subject("Welcome to BITQUE")
|
||||||
.header(ContentType::TEXT_HTML)
|
.header(ContentType::TEXT_HTML)
|
||||||
.body(html)
|
.body(html)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("{:?}", e);
|
tracing::error!("{:?}", e);
|
||||||
MailError::MailformedBody
|
MailError::MalformedBody
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
transport.send(&mail).map(|_| ()).map_err(|e| {
|
transport.send(&mail).map(|_| ()).map_err(|e| {
|
||||||
log::error!("{:?}", e);
|
tracing::error!("{:?}", e);
|
||||||
MailError::FailedToSendEmail
|
MailError::FailedToSendEmail
|
||||||
})
|
})
|
||||||
}
|
}
|
41
crates/web-actor/Cargo.toml
Normal file
41
crates/web-actor/Cargo.toml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
[package]
|
||||||
|
name = "web-actor"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||||
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "web_actor"
|
||||||
|
path = "./src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
local-storage = ["filesystem-actor"]
|
||||||
|
cloud-storage = ["cloud-storage-actor"]
|
||||||
|
default = ["local-storage"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = { version = "0.13.0" }
|
||||||
|
actix-multipart = { version = "*" }
|
||||||
|
actix-web = { version = "4" }
|
||||||
|
actix-web-actors = { version = "4" }
|
||||||
|
cloud-storage-actor = { workspace = true, optional = true }
|
||||||
|
bincode = { version = "*" }
|
||||||
|
bitque-config = { workspace = true, features = ["mail", "web", "local-storage"] }
|
||||||
|
bitque-data = { workspace = true, features = ["backend"] }
|
||||||
|
bytes = { version = "1" }
|
||||||
|
database-actor = { workspace = true }
|
||||||
|
filesystem-actor = { workspace = true, optional = true }
|
||||||
|
futures = { version = "0.3.8" }
|
||||||
|
libc = { version = "0.2.0", default-features = false }
|
||||||
|
mail-actor = { workspace = true }
|
||||||
|
openssl-sys = { version = "*", features = ["vendored"] }
|
||||||
|
serde = { version = "*" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
toml = { version = "*" }
|
||||||
|
tracing = { version = "0.1.37" }
|
||||||
|
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
|
||||||
|
websocket-actor = { workspace = true }
|
BIN
crates/web-actor/assets/LotC_Wallpaper_2560x1440.jpg
Normal file
BIN
crates/web-actor/assets/LotC_Wallpaper_2560x1440.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 MiB |
@ -2,39 +2,36 @@ use std::io::Write;
|
|||||||
|
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_multipart::{Field, Multipart};
|
use actix_multipart::{Field, Multipart};
|
||||||
use actix_web::http::header::ContentDisposition;
|
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{post, web, Error, HttpResponse};
|
use actix_web::{post, web, Error, HttpResponse};
|
||||||
use common::*;
|
use bitque_data::msg::WsMsgUser;
|
||||||
|
use bitque_data::{User, UserId};
|
||||||
use database_actor::authorize_user::AuthorizeUser;
|
use database_actor::authorize_user::AuthorizeUser;
|
||||||
use database_actor::user_projects::CurrentUserProject;
|
use database_actor::user_projects::CurrentUserProject;
|
||||||
use database_actor::users::UpdateAvatarUrl;
|
use database_actor::users::UpdateAvatarUrl;
|
||||||
use database_actor::DbExecutor;
|
use database_actor::DbExecutor;
|
||||||
#[cfg(feature = "local-storage")]
|
|
||||||
use futures::executor::block_on;
|
use futures::executor::block_on;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
use jirs_data::msg::{WsMsg, WsMsgUser};
|
use tracing::{error, warn};
|
||||||
use jirs_data::{User, UserId};
|
|
||||||
use websocket_actor::server::InnerMsg::BroadcastToChannel;
|
use websocket_actor::server::InnerMsg::BroadcastToChannel;
|
||||||
use websocket_actor::server::WsServer;
|
use websocket_actor::server::WsServer;
|
||||||
|
|
||||||
#[cfg(feature = "aws-s3")]
|
use crate::ServiceError;
|
||||||
|
|
||||||
|
#[cfg(feature = "cloud-storage")]
|
||||||
#[post("/")]
|
#[post("/")]
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
mut payload: Multipart,
|
mut payload: Multipart,
|
||||||
db: Data<Addr<DbExecutor>>,
|
db: Data<Addr<DbExecutor>>,
|
||||||
ws: Data<Addr<WsServer>>,
|
ws: Data<Addr<WsServer>>,
|
||||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let mut user_id: Option<UserId> = None;
|
let mut user_id: Option<UserId> = None;
|
||||||
let mut avatar_url: Option<String> = None;
|
let mut avatar_url: Option<String> = None;
|
||||||
|
|
||||||
while let Ok(Some(field)) = payload.try_next().await {
|
while let Ok(Some(field)) = payload.try_next().await {
|
||||||
let disposition: ContentDisposition = match field.content_disposition() {
|
let disposition = field.content_disposition();
|
||||||
Some(d) => d,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
if !disposition.is_form_data() {
|
if !disposition.is_form_data() {
|
||||||
return Ok(HttpResponse::BadRequest().finish());
|
return Ok(HttpResponse::BadRequest().finish());
|
||||||
}
|
}
|
||||||
@ -43,14 +40,16 @@ pub async fn upload(
|
|||||||
user_id = Some(handle_token(field, db.clone()).await?);
|
user_id = Some(handle_token(field, db.clone()).await?);
|
||||||
}
|
}
|
||||||
Some("avatar") => {
|
Some("avatar") => {
|
||||||
let id = user_id.ok_or_else(|| HttpResponse::Unauthorized().finish())?;
|
let Some(id) = user_id else {
|
||||||
|
warn!("user id not found. Not authorized");
|
||||||
|
return Ok(ServiceError::Unauthorized.into());
|
||||||
|
};
|
||||||
avatar_url = Some(
|
avatar_url = Some(
|
||||||
crate::handlers::upload_avatar_image::handle_image(
|
crate::handlers::upload_avatar_image::handle_image(
|
||||||
id,
|
id,
|
||||||
field,
|
field,
|
||||||
disposition,
|
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
amazon.clone(),
|
cloud_storage.clone(),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
@ -58,6 +57,10 @@ pub async fn upload(
|
|||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("user_id {user_id:?}");
|
||||||
|
tracing::info!("token {avatar_url:?}");
|
||||||
|
|
||||||
let user_id = match user_id {
|
let user_id = match user_id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
_ => return Ok(HttpResponse::Unauthorized().finish()),
|
_ => return Ok(HttpResponse::Unauthorized().finish()),
|
||||||
@ -71,34 +74,32 @@ pub async fn upload(
|
|||||||
match (user_id, avatar_url) {
|
match (user_id, avatar_url) {
|
||||||
(user_id, Some(avatar_url)) => {
|
(user_id, Some(avatar_url)) => {
|
||||||
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
||||||
ws.send(BroadcastToChannel(
|
let Ok(_) = ws.send(BroadcastToChannel(
|
||||||
project_id,
|
project_id,
|
||||||
WsMsgUser::AvatarUrlChanged(user.id, avatar_url).into(),
|
WsMsgUser::AvatarUrlChanged(user.id, avatar_url).into(),
|
||||||
))
|
))
|
||||||
.await
|
.await else {
|
||||||
.map_err(|_| HttpResponse::UnprocessableEntity().finish())?;
|
return Ok(HttpResponse::UnprocessableEntity().finish());
|
||||||
|
};
|
||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "aws-s3"))]
|
#[cfg(not(feature = "cloud-storage"))]
|
||||||
#[post("/")]
|
#[post("/")]
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
mut payload: Multipart,
|
mut payload: Multipart,
|
||||||
db: Data<Addr<DbExecutor>>,
|
db: Data<Addr<DbExecutor>>,
|
||||||
ws: Data<Addr<WsServer>>,
|
ws: Data<Addr<WsServer>>,
|
||||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let mut user_id: Option<UserId> = None;
|
let mut user_id: Option<UserId> = None;
|
||||||
let mut avatar_url: Option<String> = None;
|
let mut avatar_url: Option<String> = None;
|
||||||
|
|
||||||
while let Ok(Some(field)) = payload.try_next().await {
|
while let Ok(Some(field)) = payload.try_next().await {
|
||||||
let disposition: ContentDisposition = match field.content_disposition() {
|
let disposition = field.content_disposition();
|
||||||
Some(d) => d,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
if !disposition.is_form_data() {
|
if !disposition.is_form_data() {
|
||||||
return Ok(HttpResponse::BadRequest().finish());
|
return Ok(HttpResponse::BadRequest().finish());
|
||||||
}
|
}
|
||||||
@ -107,15 +108,10 @@ pub async fn upload(
|
|||||||
user_id = Some(handle_token(field, db.clone()).await?);
|
user_id = Some(handle_token(field, db.clone()).await?);
|
||||||
}
|
}
|
||||||
Some("avatar") => {
|
Some("avatar") => {
|
||||||
let id = user_id.ok_or_else(|| HttpResponse::Unauthorized().finish())?;
|
let Some(id) = user_id else { return Ok(HttpResponse::Unauthorized().finish()); };
|
||||||
avatar_url = Some(
|
avatar_url = Some(
|
||||||
crate::handlers::upload_avatar_image::handle_image(
|
crate::handlers::upload_avatar_image::handle_image(id, field, fs.clone())
|
||||||
id,
|
.await?,
|
||||||
field,
|
|
||||||
disposition,
|
|
||||||
fs.clone(),
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
@ -134,12 +130,16 @@ pub async fn upload(
|
|||||||
match (user_id, avatar_url) {
|
match (user_id, avatar_url) {
|
||||||
(user_id, Some(avatar_url)) => {
|
(user_id, Some(avatar_url)) => {
|
||||||
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
||||||
ws.send(BroadcastToChannel(
|
if ws
|
||||||
project_id,
|
.send(BroadcastToChannel(
|
||||||
WsMsg::User(WsMsgUser::AvatarUrlChanged(user.id, avatar_url)),
|
project_id,
|
||||||
))
|
WsMsg::User(WsMsgUser::AvatarUrlChanged(user.id, avatar_url)),
|
||||||
.await
|
))
|
||||||
.map_err(|_| HttpResponse::UnprocessableEntity().finish())?;
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Ok(HttpResponse::UnprocessableEntity().finish());
|
||||||
|
};
|
||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||||
@ -150,7 +150,7 @@ async fn update_user_avatar(
|
|||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
new_url: String,
|
new_url: String,
|
||||||
db: Data<Addr<DbExecutor>>,
|
db: Data<Addr<DbExecutor>>,
|
||||||
) -> Result<User, actix_web::Error> {
|
) -> Result<User, Error> {
|
||||||
match db
|
match db
|
||||||
.send(UpdateAvatarUrl {
|
.send(UpdateAvatarUrl {
|
||||||
user_id,
|
user_id,
|
||||||
@ -161,43 +161,42 @@ async fn update_user_avatar(
|
|||||||
Ok(Ok(user)) => Ok(user),
|
Ok(Ok(user)) => Ok(user),
|
||||||
|
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
common::log::error!("{:?}", e);
|
error!("{:?}", e);
|
||||||
Err(actix_web::Error::from(
|
Err(ServiceError::Unauthorized.into())
|
||||||
HttpResponse::Unauthorized().finish(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
common::log::error!("{:?}", e);
|
error!("{:?}", e);
|
||||||
Err(actix_web::Error::from(
|
Err(ServiceError::Unauthorized.into())
|
||||||
HttpResponse::Unauthorized().finish(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_token(
|
async fn handle_token(mut field: Field, db: Data<Addr<DbExecutor>>) -> Result<UserId, Error> {
|
||||||
mut field: Field,
|
|
||||||
db: Data<Addr<DbExecutor>>,
|
|
||||||
) -> Result<UserId, actix_web::Error> {
|
|
||||||
let mut f: Vec<u8> = vec![];
|
let mut f: Vec<u8> = vec![];
|
||||||
while let Some(chunk) = field.next().await {
|
while let Some(chunk) = field.next().await {
|
||||||
let data = chunk.unwrap();
|
let data = chunk.unwrap();
|
||||||
f = web::block(move || f.write_all(&data).map(|_| f)).await?;
|
f = web::block(move || {
|
||||||
|
if let Err(e) = f.write_all(&data) {
|
||||||
|
error!("{e}");
|
||||||
|
}
|
||||||
|
f
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
let access_token = String::from_utf8(f)
|
let access_token = String::from_utf8(f)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.parse::<uuid::Uuid>()
|
.parse::<uuid::Uuid>()
|
||||||
.map_err(|_| HttpResponse::Unauthorized().finish())?;
|
.map_err(|_| ServiceError::Unauthorized)?;
|
||||||
match db.send(AuthorizeUser { access_token }).await {
|
match db.send(AuthorizeUser { access_token }).await {
|
||||||
Ok(Ok(user)) => Ok(user.id),
|
Ok(Ok(user)) => Ok(user.id),
|
||||||
|
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
common::log::error!("{:?}", e);
|
error!("{:?}", e);
|
||||||
Err(HttpResponse::Unauthorized().finish().into())
|
Err(ServiceError::Unauthorized.into())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
common::log::error!("{:?}", e);
|
error!("{:?}", e);
|
||||||
Err(HttpResponse::Unauthorized().finish().into())
|
Err(ServiceError::Unauthorized.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,19 +1,26 @@
|
|||||||
use actix_web::HttpResponse;
|
use std::fmt::{Display, Formatter};
|
||||||
use common::*;
|
|
||||||
use jirs_data::msg::WsError;
|
use actix_web::body::BoxBody;
|
||||||
use jirs_data::ErrorResponse;
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
use bitque_data::msg::WsError;
|
||||||
|
use bitque_data::ErrorResponse;
|
||||||
|
|
||||||
const TOKEN_NOT_FOUND: &str = "Token not found";
|
const TOKEN_NOT_FOUND: &str = "Token not found";
|
||||||
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
|
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum HighlightError {
|
pub enum HighlightError {
|
||||||
UnknownLanguage,
|
UnknownLanguage,
|
||||||
UnknownTheme,
|
UnknownTheme,
|
||||||
ResultUnserializable,
|
ResultUnserializable,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AvatarError {
|
||||||
|
Upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum ServiceError {
|
pub enum ServiceError {
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
DatabaseConnectionLost,
|
DatabaseConnectionLost,
|
||||||
@ -22,6 +29,7 @@ pub enum ServiceError {
|
|||||||
RegisterCollision,
|
RegisterCollision,
|
||||||
Error(WsError),
|
Error(WsError),
|
||||||
Highlight(HighlightError),
|
Highlight(HighlightError),
|
||||||
|
Avatar(AvatarError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceError {
|
impl ServiceError {
|
||||||
@ -30,6 +38,18 @@ impl ServiceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for ServiceError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for ServiceError {
|
||||||
|
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||||
|
self.clone().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<HttpResponse> for ServiceError {
|
impl Into<HttpResponse> for ServiceError {
|
||||||
fn into(self) -> HttpResponse {
|
fn into(self) -> HttpResponse {
|
||||||
match self {
|
match self {
|
||||||
@ -70,6 +90,9 @@ impl Into<HttpResponse> for ServiceError {
|
|||||||
"Highlight succeed but result can't be send",
|
"Highlight succeed but result can't be send",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
ServiceError::Avatar(AvatarError::Upload) => {
|
||||||
|
HttpResponse::UnprocessableEntity().finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
257
crates/web-actor/src/handlers/upload_avatar_image.rs
Normal file
257
crates/web-actor/src/handlers/upload_avatar_image.rs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
use actix::{spawn, Addr};
|
||||||
|
use actix_multipart::Field;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::Error;
|
||||||
|
use bitque_data::UserId;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{AvatarError, ServiceError};
|
||||||
|
|
||||||
|
pub trait UploadField {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Read file from client
|
||||||
|
async fn read_content(&mut self, sender: Sender<Bytes>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UploadField for Field {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.content_disposition().get_filename().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_content(&mut self, sender: Sender<Bytes>) {
|
||||||
|
while let Some(chunk) = self.next().await {
|
||||||
|
let data = chunk.unwrap();
|
||||||
|
if let Err(err) = sender.send(data) {
|
||||||
|
error!("{:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "local-storage", feature = "cloud-storage"))]
|
||||||
|
pub(crate) async fn handle_image(
|
||||||
|
user_id: UserId,
|
||||||
|
mut field: impl UploadField,
|
||||||
|
local_storage: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||||
|
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let filename = field.name();
|
||||||
|
let system_file_name = format!("{}-{}", user_id, filename);
|
||||||
|
|
||||||
|
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||||
|
|
||||||
|
let results_fut = join_all([
|
||||||
|
spawn(local_storage_write(
|
||||||
|
system_file_name.clone(),
|
||||||
|
local_storage,
|
||||||
|
user_id,
|
||||||
|
sender.subscribe(),
|
||||||
|
)),
|
||||||
|
spawn(cloud_storage_write(
|
||||||
|
filename.to_string(),
|
||||||
|
cloud_storage,
|
||||||
|
format!("user_{user_id}"),
|
||||||
|
receiver,
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
let (_, results) = tokio::join!(field.read_content(sender), results_fut);
|
||||||
|
|
||||||
|
for res in results {
|
||||||
|
return if let Ok(Some(link)) = res {
|
||||||
|
Ok(link)
|
||||||
|
} else {
|
||||||
|
Err(ServiceError::Avatar(AvatarError::Upload).into())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(ServiceError::Avatar(AvatarError::Upload).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod local_and_cloud {
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use actix::SyncArbiter;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::broadcast::Sender;
|
||||||
|
|
||||||
|
use crate::handlers::upload_avatar_image::{handle_image, UploadField};
|
||||||
|
|
||||||
|
pub struct FieldMock<'s>(Cow<'s, [u8]>);
|
||||||
|
|
||||||
|
impl<'s> UploadField for FieldMock<'s> {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"foo.bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_content(&mut self, sender: Sender<Bytes>) {
|
||||||
|
loop {
|
||||||
|
if self.0.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let len = self.0.len().min(30);
|
||||||
|
let slice = self.0[0..len].to_vec();
|
||||||
|
self.0 = self.0[len..].to_vec().into();
|
||||||
|
sender.send(Bytes::copy_from_slice(&slice)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix::test]
|
||||||
|
async fn asset_test_mock() {
|
||||||
|
let v = (0..255).into_iter().collect::<Vec<_>>();
|
||||||
|
let mut field = FieldMock(v.into());
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel::<Bytes>(64);
|
||||||
|
|
||||||
|
let read = async {
|
||||||
|
spawn(async move {
|
||||||
|
let mut len = 0;
|
||||||
|
while let Ok(bytes) = rx.recv().await {
|
||||||
|
len += bytes.len();
|
||||||
|
}
|
||||||
|
len
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
let write = async move {
|
||||||
|
spawn(async move {
|
||||||
|
field.read_content(tx).await;
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
let (_, len) = tokio::join!(write, read);
|
||||||
|
assert_eq!(len.unwrap(), 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix::test]
|
||||||
|
async fn large_image() {
|
||||||
|
let field = FieldMock(Cow::Borrowed(include_bytes!(
|
||||||
|
"../../assets/LotC_Wallpaper_2560x1440.jpg"
|
||||||
|
)));
|
||||||
|
|
||||||
|
let local_storage = Data::new(SyncArbiter::start(
|
||||||
|
1,
|
||||||
|
filesystem_actor::LocalStorageExecutor::default,
|
||||||
|
));
|
||||||
|
let cloud_storage = Data::new(SyncArbiter::start(
|
||||||
|
1,
|
||||||
|
cloud_storage_actor::CloudStorageExecutor::default,
|
||||||
|
));
|
||||||
|
|
||||||
|
let res = handle_image(0, field, local_storage, cloud_storage).await;
|
||||||
|
eprintln!("{res:#?}");
|
||||||
|
res.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "local-storage"), feature = "cloud-storage"))]
|
||||||
|
pub(crate) async fn handle_image(
|
||||||
|
user_id: UserId,
|
||||||
|
mut field: impl UploadField,
|
||||||
|
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let filename = field.name();
|
||||||
|
let system_file_name = format!("{}-{}", user_id, filename);
|
||||||
|
|
||||||
|
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||||
|
|
||||||
|
let aws_fut = cloud - storage_write(system_file_name, cloud_storage, receiver);
|
||||||
|
let read_fut = field.read_content(sender);
|
||||||
|
|
||||||
|
let aws_join = tokio::task::spawn(aws_fut);
|
||||||
|
read_fut.await;
|
||||||
|
|
||||||
|
let mut new_link = None;
|
||||||
|
|
||||||
|
if let Ok(url) = aws_join.await {
|
||||||
|
new_link = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_link.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "local-storage", not(feature = "cloud-storage")))]
|
||||||
|
pub(crate) async fn handle_image(
|
||||||
|
user_id: UserId,
|
||||||
|
mut field: impl UploadField,
|
||||||
|
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let filename = field.name();
|
||||||
|
let system_file_name = format!("{}-{}", user_id, filename);
|
||||||
|
|
||||||
|
let (sender, _receiver) = tokio::sync::broadcast::channel(64);
|
||||||
|
|
||||||
|
let fs_fut = local_storage_write(system_file_name, fs, user_id, sender.subscribe());
|
||||||
|
let read_fut = field.read_content(sender);
|
||||||
|
|
||||||
|
let fs_join = tokio::task::spawn(fs_fut);
|
||||||
|
read_fut.await;
|
||||||
|
|
||||||
|
let mut new_link = None;
|
||||||
|
|
||||||
|
if let Ok(url) = fs_join.await {
|
||||||
|
new_link = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_link.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream bytes directly to Cloud Storage Service
|
||||||
|
#[cfg(feature = "cloud-storage")]
|
||||||
|
async fn cloud_storage_write(
|
||||||
|
system_file_name: String,
|
||||||
|
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||||
|
dir: String,
|
||||||
|
receiver: Receiver<Bytes>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let s3 = bitque_config::cloud_storage::config();
|
||||||
|
if !s3.active {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match cloud_storage
|
||||||
|
.send(cloud_storage_actor::PutObject {
|
||||||
|
source: receiver,
|
||||||
|
file_name: system_file_name.to_string(),
|
||||||
|
dir,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(s)) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "local-storage")]
|
||||||
|
async fn local_storage_write(
|
||||||
|
system_file_name: String,
|
||||||
|
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||||
|
_user_id: UserId,
|
||||||
|
receiver: Receiver<Bytes>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let web_config = bitque_config::web::config();
|
||||||
|
let fs_config = bitque_config::fs::config();
|
||||||
|
|
||||||
|
match fs
|
||||||
|
.send(filesystem_actor::CreateFile {
|
||||||
|
source: receiver,
|
||||||
|
file_name: system_file_name.to_string(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(_)) => Some(format!(
|
||||||
|
"{addr}{client_path}/{filename}",
|
||||||
|
addr = web_config.full_addr(),
|
||||||
|
client_path = fs_config.client_path,
|
||||||
|
filename = system_file_name
|
||||||
|
)),
|
||||||
|
Ok(_) => None,
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
|
#![feature(async_fn_in_trait)]
|
||||||
|
extern crate core;
|
||||||
|
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
use common::*;
|
use bitque_data::User;
|
||||||
use database_actor::authorize_user::AuthorizeUser;
|
use database_actor::authorize_user::AuthorizeUser;
|
||||||
use database_actor::DbExecutor;
|
use database_actor::DbExecutor;
|
||||||
pub use errors::*;
|
pub use errors::*;
|
||||||
use jirs_data::User;
|
|
||||||
|
|
||||||
use crate::middleware::authorize::token_from_headers;
|
use crate::middleware::authorize::token_from_headers;
|
||||||
|
|
@ -1,10 +1,6 @@
|
|||||||
use actix_web::http::header::{self};
|
use actix_web::http::header::{self, HeaderMap};
|
||||||
use actix_web::http::HeaderMap;
|
|
||||||
use common::*;
|
|
||||||
|
|
||||||
pub fn token_from_headers(
|
pub fn token_from_headers(headers: &HeaderMap) -> Result<uuid::Uuid, crate::errors::ServiceError> {
|
||||||
headers: &HeaderMap,
|
|
||||||
) -> std::result::Result<uuid::Uuid, crate::errors::ServiceError> {
|
|
||||||
headers
|
headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.ok_or(crate::errors::ServiceError::Unauthorized)
|
.ok_or(crate::errors::ServiceError::Unauthorized)
|
0
web/.gitignore → crates/web/.gitignore
vendored
0
web/.gitignore → crates/web/.gitignore
vendored
@ -1,55 +1,40 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "jirs_client"
|
name = "bitque_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "JIRS (Simplified JIRA in Rust) WASM client"
|
description = "BITQUE (Simplified JIRA in Rust) WASM client"
|
||||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
#license-file = "../LICENSE"
|
#license-file = "../LICENSE"
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
name = "jirs_client"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
print-model = []
|
print-model = []
|
||||||
default = []
|
default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jirs-data = { path = "../shared/jirs-data", features = ["frontend"] }
|
|
||||||
|
|
||||||
seed = { version = "0.8.0" }
|
|
||||||
|
|
||||||
serde = { version = "*" }
|
|
||||||
serde_json = { version = "*" }
|
|
||||||
bincode = { version = "*" }
|
bincode = { version = "*" }
|
||||||
|
bitque-data = { workspace = true, features = ["frontend"] }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["serde", "wasmbind"] }
|
chrono = { version = "0.4", default-features = false, features = ["serde", "wasmbind"] }
|
||||||
uuid = { version = "0.8.2", features = ["serde"] }
|
|
||||||
futures = "0.3.6"
|
|
||||||
|
|
||||||
dotenv = { version = "*" }
|
|
||||||
wasm-logger = { version = "*" }
|
|
||||||
log = "*"
|
|
||||||
|
|
||||||
console_error_panic_hook = { version = "*" }
|
console_error_panic_hook = { version = "*" }
|
||||||
|
derive_enum_iter = { workspace = true }
|
||||||
[dependencies.wee_alloc]
|
derive_enum_primitive = { workspace = true }
|
||||||
version = "*"
|
dotenv = { version = "*" }
|
||||||
features = ["static_array_backend"]
|
futures = { version = "0.3.6" }
|
||||||
|
js-sys = { version = "*", default-features = false }
|
||||||
[dependencies.wasm-bindgen]
|
seed = { version = "0", features = [] }
|
||||||
version = "*"
|
serde = { version = "1", features = ['derive'] }
|
||||||
features = ["enable-interning"]
|
serde_json = { version = "*" }
|
||||||
|
tracing = { version = "0.1.37" }
|
||||||
[dependencies.wasm-bindgen-futures]
|
tracing-subscriber = { version = "0.3.16" }
|
||||||
version = "*"
|
tracing-subscriber-wasm = { version = "0.1.0" }
|
||||||
|
uuid = { version = "1.3.0", features = ["serde"] }
|
||||||
[dependencies.js-sys]
|
wasm-bindgen = { version = "*", features = ["enable-interning"] }
|
||||||
version = "*"
|
wasm-bindgen-futures = { version = "*" }
|
||||||
default-features = false
|
wee_alloc = { version = "*", features = [] }
|
||||||
|
wasm-sockets = { version = "1", features = [] }
|
||||||
|
strum = { version = "*" }
|
||||||
|
derive_more = { version = "*" }
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "*"
|
version = "*"
|
||||||
@ -75,6 +60,7 @@ features = [
|
|||||||
"FormData",
|
"FormData",
|
||||||
"FileReader",
|
"FileReader",
|
||||||
"FileReaderSync",
|
"FileReaderSync",
|
||||||
|
"DocumentFragment",
|
||||||
"Range",
|
"Range",
|
||||||
# events
|
# events
|
||||||
"EventTarget",
|
"EventTarget",
|
||||||
@ -85,11 +71,5 @@ features = [
|
|||||||
"DragEvent",
|
"DragEvent",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies.derive_enum_primitive]
|
|
||||||
path = "../derive/derive_enum_primitive"
|
|
||||||
|
|
||||||
[dependencies.derive_enum_iter]
|
|
||||||
path = "../derive/derive_enum_iter"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = { version = "*" }
|
wasm-bindgen-test = { version = "*" }
|
29
crates/web/Dockerfile
Normal file
29
crates/web/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
FROM archlinux:latest
|
||||||
|
|
||||||
|
RUN pacman -Sy rustup gcc which --noconfirm
|
||||||
|
|
||||||
|
WORKDIR /app/
|
||||||
|
|
||||||
|
RUN rustup toolchain install nightly && \
|
||||||
|
rustup default nightly && \
|
||||||
|
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
|
||||||
|
ADD ./bitque-data /app/bitque-data
|
||||||
|
|
||||||
|
ADD ./bitque-client /app/bitque-client
|
||||||
|
|
||||||
|
RUN cd ./bitque-client && \
|
||||||
|
rm -Rf build && \
|
||||||
|
mkdir build && \
|
||||||
|
wasm-pack build --mode normal --release --out-name bitque --out-dir ./build --target web && \
|
||||||
|
cp -r ./static/* ./build && \
|
||||||
|
cat ./static/index.js \
|
||||||
|
| sed -e "s/process.env.BITQUE_SERVER_BIND/'$BITQUE_SERVER_BIND'/g" \
|
||||||
|
| sed -e "s/process.env.BITQUE_SERVER_PORT/'$BITQUE_SERVER_PORT'/g" &> ./build/index.js && \
|
||||||
|
cp ./js/template.html ./build/index.html && \
|
||||||
|
mkdir -p /assets && \
|
||||||
|
cp -r ./build/* /assets
|
||||||
|
|
||||||
|
CMD cat /app/bitque-client/static/index.js \
|
||||||
|
| sed -e "s/process.env.BITQUE_SERVER_BIND/'$BITQUE_SERVER_BIND'/g" \
|
||||||
|
| sed -e "s/process.env.BITQUE_SERVER_PORT/'$BITQUE_SERVER_PORT'/g" &> /assets/index.js
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user