Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deploy 20240503 #292

Merged
merged 20 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
134c439
feat: 申請のバリデーションを追加
arata-nvm May 1, 2024
68c60bd
Merge pull request #282 from sohosai/feature/form-validation
arata-nvm May 1, 2024
09ce327
docs: READMEを追記
arata-nvm May 1, 2024
c29993b
Merge pull request #283 from sohosai/feature/readme
arata-nvm May 1, 2024
588124d
feat: 申請エクスポート時に、対象となる全ての企画を含めるように
arata-nvm May 1, 2024
8624e7d
Merge pull request #284 from sohosai/feature/export-form-answer
arata-nvm May 1, 2024
653388c
refactor: infrastructureのモジュール構造を変更
arata-nvm May 3, 2024
27e1bd3
feat: Slack通知のadapterを作成
arata-nvm May 3, 2024
4367145
refactor: URLの生成処理を分離
arata-nvm May 3, 2024
ff47d7b
feat: 申請・お知らせ・企画作成じに通知を送信するように
arata-nvm May 3, 2024
4cc3403
feat: Slack通知を実装
arata-nvm May 3, 2024
042ab42
Merge pull request #287 from sohosai/feature/notification
arata-nvm May 3, 2024
925534b
fix: エクスポート時の日時の形式を修正
arata-nvm May 3, 2024
6b0b9fa
chore: docker-compose.ymlを修正
arata-nvm May 3, 2024
d1bb1a7
Merge pull request #289 from sohosai/feature/export-datetime
arata-nvm May 3, 2024
3dfedf0
Merge pull request #288 from sohosai/feature/fix-docker-compose
arata-nvm May 3, 2024
3dbd828
chore: docker-compose.ymlを修正
arata-nvm May 3, 2024
163d5d5
Merge pull request #290 from sohosai/feature/fix-docker-compose
arata-nvm May 3, 2024
d1944b7
fix: Slack通知のURLを実委人向けのものに修正
arata-nvm May 3, 2024
d6e14b6
Merge pull request #291 from sohosai/feature/fix-slack-notify
arata-nvm May 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
POSTGRES_HOSTNAME=postgres
POSTGRES_HOSTNAME=localhost
POSTGRES_PORT=5432

POSTGRES_DB_URL=postgresql://postgres:postgres@postgres:5432/postgres
POSTGRES_DB_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}"

# For sqlx::test
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}"

MONGO_INITDB_ROOT_USERNAME=mongo
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=password

MONGO_DB=mongo
MONGO_DB_URL="mongodb://mongo:password:27017"
MONGO_DB_URL="mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@localhost:27017"

ME_CONFIG_MONGODB_ADMINUSERNAME=root
ME_CONFIG_MONGODB_ADMINPASSWORD=password
ME_CONFIG_MONGODB_SERVER=mongo

PORT=8080
FIREBASE_PROJECT_ID=""
FIREBASE_SERVICE_ACCOUNT_KEY=""
PROJECT_APPLICATION_START_AT="2024-03-16T15:00:00Z"
PROJECT_APPLICATION_END_AT="2024-03-17T15:00:00Z"
REQUIRE_EMAIL_VERIFICATION=false
S3_ENDPOINT=http://s3.isk01.sakurastorage.jp
S3_REGION=jp-north-1
PROJECT_APPLICATION_START_AT="2024-03-15T22:19:08+09:00"
PROJECT_APPLICATION_END_AT="2024-04-10T22:19:08+09:00"
REQUIRE_EMAIL_VERIFICATION=true

S3_ENDPOINT=""
S3_REGION=""
S3_BUCKET=""
S3_ACCESS_KEY_ID=""
S3_SECRET_ACCESS_KEY=""

PORT=8080

SEND_GRID_API_KEY=""
EMAIL_SENDER_ADDRESS=""
EMAIL_REPLY_TO_ADDRESS=""
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 11 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,28 @@
# sos24-server

[![CD(prodution)](https://github.com/sohosai/sos24-server/actions/workflows/cd.yaml/badge.svg)](https://github.com/sohosai/sos24-server/actions/workflows/cd.yaml)
[![CD(beta)](https://github.com/sohosai/sos24-server/actions/workflows/cd-beta.yaml/badge.svg)](https://github.com/sohosai/sos24-server/actions/workflows/cd-beta.yaml)
[![CD(staging)](https://github.com/sohosai/sos24-server/actions/workflows/cd-staging.yaml/badge.svg)](https://github.com/sohosai/sos24-server/actions/workflows/cd-staging.yaml)

雙峰祭オンラインシステムのサーバーです。

> [!NOTE]
> クエリを変更した場合はCIを通すために `cargo sqlx prepare --workspace` を実行してください。

## sos24-presentation

sos24-presentationクレートは、アプリケーションのHTTPサーバーとしての役割を果たしています。このクレートは、HTTPリクエストを適切なユースケースにルーティングし、その結果をHTTPレスポンスとしてクライアントに返します。

具体的には、create_app関数でHTTPルーターを作成し、それぞれのエンドポイント(例えば、ニュースやユーザーに関するエンドポイント)を適切なハンドラー関数にルーティングします。これらのハンドラー関数は、Modules構造体を通じてユースケースにアクセスします。

Modules構造体は、アプリケーションの設定と、ニュースとユーザーに関するユースケースを保持します。これらのユースケースは、sos24-use-caseクレートで定義されています。

また、ToStatusCodeトレイトは、エラーをHTTPステータスコードに変換するために使用されます。

## sos24-use-case

sos24-use-caseクレートは、アプリケーションのビジネスロジックを実装しています。このクレートは、アプリケーションのユースケース(操作)を表現するためのインターフェースを提供します。

具体的には、UserUseCaseとNewsUseCaseの2つの主要なユースケースがあります。

UserUseCaseは、ユーザーに関連する操作を提供します。これには、ユーザーの作成(createメソッド)、特定のIDを持つユーザーの検索(find_by_idメソッド)などが含まれます。

一方、NewsUseCaseは、ニュースに関連する操作を提供します。これには、ニュースの作成(createメソッド)、ニュースのリスト取得(listメソッド)などが含まれます。

これらのユースケースは、Repositoriesトレイトを通じてデータストレージにアクセスします。このトレイトは、sos24-domainクレートで定義されたエンティティを操作するためのメソッドを提供します。

また、このクレートは、UserDtoやCreateUserDtoなどのデータ転送オブジェクト(DTO)を定義しています。これらのDTOは、ユースケースとプレゼンテーション層との間でデータをやり取りするために使用されます。

## sos24-domain

sos24-domainクレートは、アプリケーションのドメインモデルを定義しています。このクレートは、アプリケーションのビジネスロジックの核心部分を表現するエンティティと、それらのエンティティを永続化するためのリポジトリインターフェースを提供します。

具体的には、entityモジュールとrepositoryモジュールが主要な部分です。

entityやNewsなどのエンティティが定義されています。これらのエンティティは、アプリケーションのビジネスルールをカプセル化し、アプリケーションの状態を表現します。

一方、repositoryモジュールでは、これらのエンティティを永続化するためのインターフェースが定義されています。これにより、ビジネスロジックは永続化の詳細から分離され、異なる永続化メカニズムを容易に切り替えることができます。

また、このクレートはtestモジュールも提供しています。これは、ドメインモデルの単体テストをサポートするためのユーティリティを提供します。
## 環境構築

このように、sos24-domainクレートは、アプリケーションのビジネスロジックを表現し、そのロジックをテストし、永続化するための基盤を提供します。
### 環境変数

## sos24-infrastructure
`.env.sample`を参考に`.env`ファイルを作成し、環境変数を設定してください。

sos24-infrastructureクレートは、アプリケーションのインフラストラクチャ層を定義しています。このクレートは、アプリケーションのビジネスロジックをサポートするための具体的な永続化メカニズムと外部サービスの実装を提供します。
### データベースのセットアップ

具体的には、firebaseモジュールとpostgresqlモジュールが主要な部分です
`cargo install sqlx-cli`で`sqlx-cli`をインストールします。その後`cargo sqlx database create`でデータベースを作成し、`cargo sqlx migrate run`でマイグレーションを実行します

firebaseモジュールでは、Firebaseを使用したユーザー認証の実装が提供されています。FirebaseUserRepositoryImplクラスは、Firebaseを使用してユーザー情報を取得するための具体的なメカニズムを提供します。
### ビルド

一方、postgresqlモジュールでは、PostgreSQLを使用したデータの永続化の実装が提供されています。PgNewsRepositoryやPgUserRepositoryクラスは、それぞれニュースとユーザー情報をPostgreSQLデータベースに永続化するための具体的なメカニズムを提供します
`cargo run --bin sos24-presentation`でサーバーを起動します

また、DefaultRepositoriesクラスは、これらの具体的なリポジトリの実装をまとめ、sos24-domainクレートで定義されたリポジトリインターフェースを満たす形で提供します。
### テスト

このように、sos24-infrastructureクレートは、アプリケーションのビジネスロジックをサポートするための具体的なインフラストラクチャを提供します
`cargo test`もしくは`cargo nextest run`でテストを実行します
201 changes: 185 additions & 16 deletions crates/sos24-domain/src/entity/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ use super::permission::{PermissionDeniedError, Permissions};
pub enum FormError {
#[error("The end time is earlier than the start time")]
EndTimeEarlierThanStartTime,
#[error("The minimum length is greater than the maximum length")]
MinLengthGreaterThanMaxLength,
#[error("The minimum value is greater than the maximum value")]
MinGreaterThanMax,
#[error("The options is empty")]
EmptyOptions,
#[error("The minimum selection is greater than the options")]
MinSelectionGreaterThanOptions,
#[error("The minimum selection is greater than the maximum selection")]
MinSelectionGreaterThanMaxSelection,
}

#[derive(Debug, Clone, PartialEq, Eq, Getters)]
Expand Down Expand Up @@ -365,32 +375,64 @@ impl FormItemKind {
min_length: Option<FormItemMinLength>,
max_length: Option<FormItemMaxLength>,
allow_newline: FormItemAllowNewline,
) -> Self {
Self::String(FormItemString {
) -> Result<Self, FormError> {
if let (Some(min_length), Some(max_length)) = (min_length.clone(), max_length.clone()) {
if min_length.value() > max_length.value() {
return Err(FormError::MinLengthGreaterThanMaxLength);
}
}

Ok(Self::String(FormItemString {
min_length,
max_length,
allow_newline,
})
}))
}

pub fn new_int(min: Option<FormItemMin>, max: Option<FormItemMax>) -> Self {
Self::Int(FormItemInt { min, max })
pub fn new_int(min: Option<FormItemMin>, max: Option<FormItemMax>) -> Result<Self, FormError> {
if let (Some(min), Some(max)) = (min.clone(), max.clone()) {
if min.value() > max.value() {
return Err(FormError::MinGreaterThanMax);
}
}

Ok(Self::Int(FormItemInt { min, max }))
}

pub fn new_choose_one(options: Vec<FormItemOption>) -> Self {
Self::ChooseOne(FormItemChooseOne { options })
pub fn new_choose_one(options: Vec<FormItemOption>) -> Result<Self, FormError> {
if options.is_empty() {
return Err(FormError::EmptyOptions);
}

Ok(Self::ChooseOne(FormItemChooseOne { options }))
}

pub fn new_choose_many(
options: Vec<FormItemOption>,
min_selection: Option<FormItemMinSelection>,
max_selection: Option<FormItemMaxSelection>,
) -> Self {
Self::ChooseMany(FormItemChooseMany {
) -> Result<Self, FormError> {
if options.is_empty() {
return Err(FormError::EmptyOptions);
}
if let Some(min_selection) = min_selection.clone() {
if min_selection.value() > options.len() as u32 {
return Err(FormError::MinSelectionGreaterThanOptions);
}
}
if let (Some(min_selection), Some(max_selection)) =
(min_selection.clone(), max_selection.clone())
{
if min_selection.value() > max_selection.value() {
return Err(FormError::MinSelectionGreaterThanMaxSelection);
}
}

Ok(Self::ChooseMany(FormItemChooseMany {
options,
min_selection,
max_selection,
})
}))
}

pub fn new_file(
Expand Down Expand Up @@ -520,23 +562,45 @@ pub struct DestructedFormItemFile {
pub limit: Option<FormItemLimit>,
}

impl_value_object!(FormItemMinLength(i32));
impl_value_object!(FormItemMaxLength(i32));
impl_value_object!(FormItemMinLength(u32));
impl_value_object!(FormItemMaxLength(u32));
impl_value_object!(FormItemAllowNewline(bool));
impl_value_object!(FormItemMin(i32));
impl_value_object!(FormItemMax(i32));
impl_value_object!(FormItemOption(String));
impl_value_object!(FormItemMinSelection(i32));
impl_value_object!(FormItemMaxSelection(i32));
impl_value_object!(FormItemMinSelection(u32));
impl_value_object!(FormItemMaxSelection(u32));
impl_value_object!(FormItemExtension(String));
impl_value_object!(FormItemLimit(i32));
impl_value_object!(FormItemLimit(u32));

#[cfg(test)]
mod tests {
use crate::{entity::form::FormError, test::fixture};
use crate::{
entity::form::{
FormError, FormItemAllowNewline, FormItemKind, FormItemMax, FormItemMaxLength,
FormItemMaxSelection, FormItemMin, FormItemMinLength, FormItemMinSelection,
FormItemOption,
},
test::fixture,
};

use super::Form;

#[test]
fn 申請の開始時間が終了時間より前ならばエラーを返さない() {
let form = Form::create(
fixture::form::title1(),
fixture::form::description1(),
fixture::form::starts_at1(),
fixture::form::ends_at1(),
fixture::form::categories1(),
fixture::form::attributes1(),
fixture::form::items1(),
fixture::form::attachments1(),
);
assert!(form.is_ok());
}

#[test]
fn 申請の開始時間が終了時間より後ならばエラーを返す() {
let form = Form::create(
Expand All @@ -551,4 +615,109 @@ mod tests {
);
assert!(matches!(form, Err(FormError::EndTimeEarlierThanStartTime)));
}

#[test]
fn 文字列項目の最小文字数が最大文字数以下ならばエラーを返さない() {
let item = FormItemKind::new_string(
Some(FormItemMinLength::new(1)),
Some(FormItemMaxLength::new(2)),
FormItemAllowNewline::new(false),
);
assert!(item.is_ok());
}

#[test]
fn 文字列項目の最小文字数が最大文字数より大きいならばエラーを返す() {
let item = FormItemKind::new_string(
Some(FormItemMinLength::new(2)),
Some(FormItemMaxLength::new(1)),
FormItemAllowNewline::new(false),
);
assert!(matches!(
item,
Err(FormError::MinLengthGreaterThanMaxLength)
));
}

#[test]
fn 数値項目の最小値が最大値以下ならばエラーを返さない() {
let item = FormItemKind::new_int(Some(FormItemMin::new(1)), Some(FormItemMax::new(2)));
assert!(item.is_ok());
}

#[test]
fn 数値項目の最小値が最大値より大きいならばエラーを返す() {
let item = FormItemKind::new_int(Some(FormItemMin::new(2)), Some(FormItemMax::new(1)));
assert!(matches!(item, Err(FormError::MinGreaterThanMax)));
}

#[test]
fn 選択肢項目の選択肢が1個以上ならばエラーを返さない() {
let item = FormItemKind::new_choose_one(vec![FormItemOption::new("a".to_string())]);
assert!(item.is_ok());
}

#[test]
fn 選択肢項目の選択肢が0個ならばエラーを返す() {
let item = FormItemKind::new_choose_one(vec![]);
assert!(matches!(item, Err(FormError::EmptyOptions)));
}

#[test]
fn 複数選択項目の最小選択数が選択肢数以下ならばエラーを返さない() {
let item = FormItemKind::new_choose_many(
vec![
FormItemOption::new("a".to_string()),
FormItemOption::new("b".to_string()),
],
Some(FormItemMinSelection::new(2)),
None,
);
assert!(item.is_ok());
}

#[test]
fn 複数選択項目の最小選択数が選択肢数より大きいならばエラーを返す() {
let item = FormItemKind::new_choose_many(
vec![
FormItemOption::new("a".to_string()),
FormItemOption::new("b".to_string()),
],
Some(FormItemMinSelection::new(3)),
None,
);
assert!(matches!(
item,
Err(FormError::MinSelectionGreaterThanOptions)
));
}

#[test]
fn 複数選択項目の最小選択数が最大選択数以下ならばエラーを返さない() {
let item = FormItemKind::new_choose_many(
vec![
FormItemOption::new("a".to_string()),
FormItemOption::new("b".to_string()),
],
Some(FormItemMinSelection::new(1)),
Some(FormItemMaxSelection::new(2)),
);
assert!(item.is_ok());
}

#[test]
fn 複数選択項目の最小選択数が最大選択数より大きいならばエラーを返す() {
let item = FormItemKind::new_choose_many(
vec![
FormItemOption::new("a".to_string()),
FormItemOption::new("b".to_string()),
],
Some(FormItemMinSelection::new(2)),
Some(FormItemMaxSelection::new(1)),
);
assert!(matches!(
item,
Err(FormError::MinSelectionGreaterThanMaxSelection)
));
}
}
Loading