├── .editorconfig ├── .envrc.sample ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── audit.yml │ ├── beta.yml │ ├── ci.yml │ ├── generate-documentation.yml │ ├── main.yml │ ├── nightly.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.nix ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── VERSION ├── default.nix ├── docker-compose.dev.yml ├── docker-compose.run.yml ├── nix ├── crate2nix.nix ├── deployment │ ├── acme.nix │ ├── beta.nix │ ├── main.nix │ ├── nginx.nix │ ├── nightly.nix │ ├── production.nix │ ├── sos21-api-server.nix │ └── staging.nix ├── ignore-generate-lockfile-cargo.nix ├── image.nix ├── overlay.nix ├── pkgs.nix ├── rustPlatform.nix └── sqlx-cli.nix ├── script ├── flush_changelog.sh ├── shell.nix └── update_version.sh ├── shell.nix ├── sos21-api-server ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── build.rs ├── default.nix ├── schema │ ├── api.yml │ ├── model │ │ ├── DateTime.yml │ │ ├── DistributedFile.yml │ │ ├── Mime.yml │ │ ├── Null.yml │ │ ├── ProjectQuery.yml │ │ ├── error │ │ │ ├── Error.yml │ │ │ └── ErrorBody.yml │ │ ├── file │ │ │ ├── File.yml │ │ │ └── FileId.yml │ │ ├── file_distribution │ │ │ ├── FileDistribution.yml │ │ │ └── FileDistributionId.yml │ │ ├── file_sharing │ │ │ ├── FileSharing.yml │ │ │ ├── FileSharingId.yml │ │ │ └── FileSharingScope.yml │ │ ├── form │ │ │ ├── Form.yml │ │ │ ├── FormCondition.yml │ │ │ ├── FormId.yml │ │ │ └── item │ │ │ │ ├── CheckboxId.yml │ │ │ │ ├── FormItem.yml │ │ │ │ ├── FormItemCondition.yml │ │ │ │ ├── FormItemId.yml │ │ │ │ ├── GridRadioColumnId.yml │ │ │ │ ├── GridRadioRowId.yml │ │ │ │ └── RadioId.yml │ │ ├── form_answer │ │ │ ├── FormAnswer.yml │ │ │ ├── FormAnswerId.yml │ │ │ ├── FormAnswerItem.yml │ │ │ └── RequestFormAnswerItem.yml │ │ ├── pending_project │ │ │ ├── PendingProject.yml │ │ │ └── PendingProjectId.yml │ │ ├── project │ │ │ ├── Project.yml │ │ │ ├── ProjectAttribute.yml │ │ │ ├── ProjectCategory.yml │ │ │ └── ProjectId.yml │ │ ├── registration_form │ │ │ ├── RegistrationForm.yml │ │ │ └── RegistrationFormId.yml │ │ ├── registration_form_answer │ │ │ ├── RegistrationFormAnswer.yml │ │ │ └── RegistrationFormAnswerId.yml │ │ ├── user │ │ │ ├── User.yml │ │ │ ├── UserCategory.yml │ │ │ ├── UserId.yml │ │ │ ├── UserKanaName.yml │ │ │ ├── UserName.yml │ │ │ └── UserRole.yml │ │ └── user_invitation │ │ │ ├── UserInvitation.yml │ │ │ ├── UserInvitationId.yml │ │ │ └── UserInvitationRole.yml │ ├── package-lock.json │ └── package.json └── src │ ├── app.rs │ ├── config.rs │ ├── filter.rs │ ├── filter │ ├── authentication.rs │ ├── authentication │ │ ├── bearer.rs │ │ ├── claim.rs │ │ └── key_store.rs │ ├── error.rs │ └── error │ │ └── model.rs │ ├── handler.rs │ ├── handler │ ├── assign_user_role_to_email.rs │ ├── file.rs │ ├── file │ │ ├── create.rs │ │ ├── get.rs │ │ ├── get_info.rs │ │ └── share.rs │ ├── file_distribution.rs │ ├── file_distribution │ │ ├── create.rs │ │ ├── get.rs │ │ └── list.rs │ ├── file_sharing.rs │ ├── file_sharing │ │ ├── get.rs │ │ ├── get_file.rs │ │ ├── get_file_info.rs │ │ ├── get_public_file.rs │ │ ├── get_public_file_info.rs │ │ ├── public.rs │ │ └── revoke.rs │ ├── form.rs │ ├── form │ │ ├── answer.rs │ │ ├── answer │ │ │ ├── export.rs │ │ │ └── list.rs │ │ ├── create.rs │ │ ├── get.rs │ │ ├── list.rs │ │ └── update.rs │ ├── form_answer.rs │ ├── form_answer │ │ ├── file_sharing.rs │ │ ├── file_sharing │ │ │ ├── get_file.rs │ │ │ └── get_file_info.rs │ │ └── get.rs │ ├── invite_user.rs │ ├── me.rs │ ├── me │ │ ├── file.rs │ │ ├── file │ │ │ ├── check_usage.rs │ │ │ └── list.rs │ │ ├── file_sharing.rs │ │ ├── file_sharing │ │ │ └── list.rs │ │ ├── get.rs │ │ ├── pending_project.rs │ │ ├── pending_project │ │ │ └── get.rs │ │ ├── project.rs │ │ └── project │ │ │ └── get.rs │ ├── meta.rs │ ├── meta │ │ ├── get_build_info.rs │ │ ├── health.rs │ │ └── health │ │ │ ├── check.rs │ │ │ └── check_liveness.rs │ ├── model.rs │ ├── model │ │ ├── date_time.rs │ │ ├── distributed_file.rs │ │ ├── file.rs │ │ ├── file_distribution.rs │ │ ├── file_sharing.rs │ │ ├── form.rs │ │ ├── form │ │ │ ├── item.rs │ │ │ └── item │ │ │ │ ├── checkbox.rs │ │ │ │ ├── condition.rs │ │ │ │ ├── grid_radio.rs │ │ │ │ └── radio.rs │ │ ├── form_answer.rs │ │ ├── form_answer │ │ │ └── item.rs │ │ ├── pending_project.rs │ │ ├── project.rs │ │ ├── project_query.rs │ │ ├── registration_form.rs │ │ ├── registration_form_answer.rs │ │ ├── serde.rs │ │ ├── user.rs │ │ └── user_invitation.rs │ ├── pending_project.rs │ ├── pending_project │ │ ├── get.rs │ │ ├── registration_form.rs │ │ ├── registration_form │ │ │ ├── answer.rs │ │ │ ├── answer │ │ │ │ ├── get.rs │ │ │ │ └── update.rs │ │ │ ├── get.rs │ │ │ └── list.rs │ │ ├── update.rs │ │ └── update_any.rs │ ├── project.rs │ ├── project │ │ ├── create.rs │ │ ├── export.rs │ │ ├── file_distribution.rs │ │ ├── file_distribution │ │ │ ├── get.rs │ │ │ └── list.rs │ │ ├── file_sharing.rs │ │ ├── file_sharing │ │ │ ├── get_file.rs │ │ │ └── get_file_info.rs │ │ ├── form.rs │ │ ├── form │ │ │ ├── answer.rs │ │ │ ├── answer │ │ │ │ ├── file_sharing.rs │ │ │ │ ├── file_sharing │ │ │ │ │ ├── get_file.rs │ │ │ │ │ └── get_file_info.rs │ │ │ │ ├── get.rs │ │ │ │ └── update.rs │ │ │ ├── get.rs │ │ │ └── list.rs │ │ ├── get.rs │ │ ├── list.rs │ │ ├── prepare.rs │ │ ├── registration_form.rs │ │ ├── registration_form │ │ │ ├── answer.rs │ │ │ ├── answer │ │ │ │ ├── file_sharing.rs │ │ │ │ ├── file_sharing │ │ │ │ │ ├── get_file.rs │ │ │ │ │ └── get_file_info.rs │ │ │ │ ├── get.rs │ │ │ │ └── update.rs │ │ │ ├── get.rs │ │ │ └── list.rs │ │ ├── update.rs │ │ └── update_any.rs │ ├── project_creation_availability.rs │ ├── project_creation_availability │ │ └── get.rs │ ├── registration_form.rs │ ├── registration_form │ │ ├── answer.rs │ │ ├── answer │ │ │ ├── export.rs │ │ │ └── list.rs │ │ ├── create.rs │ │ ├── get.rs │ │ └── list.rs │ ├── registration_form_answer.rs │ ├── registration_form_answer │ │ ├── file_sharing.rs │ │ ├── file_sharing │ │ │ ├── get_file.rs │ │ │ └── get_file_info.rs │ │ └── get.rs │ ├── signup.rs │ ├── user.rs │ ├── user │ │ ├── export.rs │ │ ├── get.rs │ │ ├── list.rs │ │ └── update.rs │ ├── user_invitation.rs │ └── user_invitation │ │ ├── delete.rs │ │ ├── get.rs │ │ └── list.rs │ ├── lib.rs │ ├── main.rs │ └── server.rs ├── sos21-database ├── Cargo.toml ├── README.md ├── default.nix ├── migrations │ ├── 20210122032913_init.sql │ ├── 20210302103321_add_project_index.sql │ ├── 20210302170442_add_cooking_food_outdoor.sql │ ├── 20210302200024_delete_display_id.sql │ ├── 20210309032212_structured_project_query.sql │ ├── 20210311070210_json_form_data.sql │ ├── 20210311073742_delete_unused_count_project_sequence.sql │ ├── 20210311165742_add_files.sql │ ├── 20210320113927_add_file_sharings_and_distributions.sql │ ├── 20210330101458_pending_project_subowner.sql │ ├── 20210412123257_user_category.sql │ ├── 20210420201104_registaration_form.sql │ ├── 20210425183738_unique_owner_subowner.sql │ ├── 20210429135555_user_invitation.sql │ ├── 20210520143144_affiliation_user_category.sql │ ├── 20210615083430_add_project_query_file_sharing.sql │ ├── 20210715163345_last_update.sql │ ├── 20220412141856_delete_affiliation.sql │ ├── 20220414211708_add-project-category-enum.sql │ ├── 20220425063347_slack-notification.sql │ ├── 20220622022603_exceptional_complete_deadline.sql │ └── 20230423025208_redefine_project_category.sql ├── sqlx-data.json └── src │ ├── command.rs │ ├── command │ ├── delete_file_distribution_files.rs │ ├── delete_form_condition_excludes.rs │ ├── delete_form_condition_includes.rs │ ├── delete_form_project_query_conjunctions.rs │ ├── delete_pending_project.rs │ ├── delete_registration_form_project_query_conjunctions.rs │ ├── delete_user_invitation.rs │ ├── insert_file.rs │ ├── insert_file_distribution.rs │ ├── insert_file_distribution_files.rs │ ├── insert_file_sharing.rs │ ├── insert_form.rs │ ├── insert_form_answer.rs │ ├── insert_form_condition_excludes.rs │ ├── insert_form_condition_includes.rs │ ├── insert_form_project_query_conjunctions.rs │ ├── insert_pending_project.rs │ ├── insert_project.rs │ ├── insert_registration_form.rs │ ├── insert_registration_form_answer.rs │ ├── insert_registration_form_project_query_conjunctions.rs │ ├── insert_user.rs │ ├── insert_user_invitation.rs │ ├── update_file.rs │ ├── update_file_distribution.rs │ ├── update_file_sharing.rs │ ├── update_form.rs │ ├── update_form_answer.rs │ ├── update_pending_project.rs │ ├── update_project.rs │ ├── update_registration_form.rs │ ├── update_registration_form_answer.rs │ ├── update_user.rs │ └── update_user_invitation.rs │ ├── lib.rs │ ├── model.rs │ ├── model │ ├── file.rs │ ├── file_distribution.rs │ ├── file_sharing.rs │ ├── form.rs │ ├── form_answer.rs │ ├── pending_project.rs │ ├── project.rs │ ├── registration_form.rs │ ├── registration_form_answer.rs │ ├── user.rs │ └── user_invitation.rs │ ├── query.rs │ └── query │ ├── count_projects.rs │ ├── count_registration_form_answers_by_pending_project.rs │ ├── count_registration_forms_by_pending_project.rs │ ├── find_file.rs │ ├── find_file_distribution.rs │ ├── find_file_sharing.rs │ ├── find_form.rs │ ├── find_form_answer.rs │ ├── find_form_answer_by_form_and_project.rs │ ├── find_pending_project.rs │ ├── find_project.rs │ ├── find_project_by_index.rs │ ├── find_registration_form.rs │ ├── find_registration_form_answer.rs │ ├── find_registration_form_answer_by_registration_form_and_pending_project.rs │ ├── find_registration_form_answer_by_registration_form_and_project.rs │ ├── find_user.rs │ ├── find_user_by_email.rs │ ├── find_user_invitation.rs │ ├── find_user_invitation_by_email.rs │ ├── get_next_index.rs │ ├── is_healthy.rs │ ├── list_file_distributions.rs │ ├── list_file_distributions_by_project.rs │ ├── list_file_sharings_by_pending_project.rs │ ├── list_file_sharings_by_user.rs │ ├── list_files_by_user.rs │ ├── list_form_answers_by_form.rs │ ├── list_forms.rs │ ├── list_forms_by_project.rs │ ├── list_projects.rs │ ├── list_registration_form_answers_by_pending_project.rs │ ├── list_registration_form_answers_by_registration_form.rs │ ├── list_registration_forms.rs │ ├── list_registration_forms_by_pending_project.rs │ ├── list_registration_forms_by_project.rs │ ├── list_user_invitations.rs │ ├── list_users.rs │ └── sum_file_size_by_user.rs ├── sos21-domain ├── Cargo.toml ├── README.md ├── default.nix └── src │ ├── context.rs │ ├── context │ ├── authentication.rs │ ├── config.rs │ ├── file_distribution_repository.rs │ ├── file_repository.rs │ ├── file_sharing_repository.rs │ ├── form_answer_repository.rs │ ├── form_repository.rs │ ├── login.rs │ ├── object_repository.rs │ ├── pending_project_repository.rs │ ├── project_repository.rs │ ├── registration_form_answer_repository.rs │ ├── registration_form_repository.rs │ ├── user_invitation_repository.rs │ └── user_repository.rs │ ├── error.rs │ ├── lib.rs │ ├── model.rs │ ├── model │ ├── bound.rs │ ├── collection.rs │ ├── date_time.rs │ ├── email.rs │ ├── file.rs │ ├── file │ │ ├── digest.rs │ │ ├── name.rs │ │ ├── size.rs │ │ └── type_.rs │ ├── file_distribution.rs │ ├── file_distribution │ │ ├── description.rs │ │ ├── distributed_file.rs │ │ ├── files.rs │ │ └── name.rs │ ├── file_sharing.rs │ ├── file_sharing │ │ ├── scope.rs │ │ └── state.rs │ ├── form.rs │ ├── form │ │ ├── condition.rs │ │ ├── description.rs │ │ ├── item.rs │ │ ├── item │ │ │ ├── checkbox.rs │ │ │ ├── checkbox │ │ │ │ ├── checkbox.rs │ │ │ │ └── limit.rs │ │ │ ├── condition.rs │ │ │ ├── description.rs │ │ │ ├── file.rs │ │ │ ├── file │ │ │ │ └── types.rs │ │ │ ├── grid_radio.rs │ │ │ ├── grid_radio │ │ │ │ ├── column.rs │ │ │ │ └── row.rs │ │ │ ├── integer.rs │ │ │ ├── integer │ │ │ │ ├── limit.rs │ │ │ │ └── unit.rs │ │ │ ├── name.rs │ │ │ ├── radio.rs │ │ │ ├── radio │ │ │ │ └── radio.rs │ │ │ ├── text.rs │ │ │ └── text │ │ │ │ ├── length.rs │ │ │ │ └── placeholder.rs │ │ ├── name.rs │ │ └── period.rs │ ├── form_answer.rs │ ├── form_answer │ │ ├── item.rs │ │ └── item │ │ │ ├── checks.rs │ │ │ ├── file_sharings.rs │ │ │ ├── grid_rows.rs │ │ │ └── text.rs │ ├── integer.rs │ ├── object.rs │ ├── object │ │ └── data.rs │ ├── pending_project.rs │ ├── permissions.rs │ ├── phone_number.rs │ ├── project.rs │ ├── project │ │ ├── attribute.rs │ │ ├── category.rs │ │ ├── code.rs │ │ ├── description.rs │ │ ├── index.rs │ │ └── name.rs │ ├── project_creation_period.rs │ ├── project_query.rs │ ├── registration_form.rs │ ├── registration_form │ │ ├── description.rs │ │ └── name.rs │ ├── registration_form_answer.rs │ ├── registration_form_answer │ │ └── respondent.rs │ ├── string.rs │ ├── user.rs │ ├── user │ │ ├── assignment.rs │ │ ├── category.rs │ │ ├── email.rs │ │ ├── file_usage.rs │ │ ├── file_usage_quota.rs │ │ ├── name.rs │ │ └── role.rs │ ├── user_invitation.rs │ └── user_invitation │ │ └── role.rs │ ├── test.rs │ └── test │ ├── context.rs │ ├── model.rs │ └── model │ ├── file.rs │ ├── file_distribution.rs │ ├── file_sharing.rs │ ├── form.rs │ ├── form │ └── item.rs │ ├── form_answer.rs │ ├── object.rs │ ├── pending_project.rs │ ├── project.rs │ ├── project_creation_period.rs │ ├── project_query.rs │ ├── registration_form.rs │ ├── registration_form_answer.rs │ ├── user.rs │ └── user_invitation.rs ├── sos21-gateway ├── database │ ├── Cargo.toml │ ├── README.md │ ├── default.nix │ └── src │ │ ├── file_distribution_repository.rs │ │ ├── file_repository.rs │ │ ├── file_sharing_repository.rs │ │ ├── form_answer_repository.rs │ │ ├── form_repository.rs │ │ ├── lib.rs │ │ ├── pending_project_repository.rs │ │ ├── project_repository.rs │ │ ├── registration_form_answer_repository.rs │ │ ├── registration_form_repository.rs │ │ ├── user_invitation_repository.rs │ │ └── user_repository.rs ├── s3 │ ├── Cargo.toml │ ├── README.md │ ├── default.nix │ └── src │ │ ├── lib.rs │ │ └── object_repository.rs └── slack │ ├── Cargo.toml │ └── src │ └── lib.rs ├── sos21-run-migrations ├── Cargo.toml ├── README.md ├── default.nix └── src │ └── main.rs └── sos21-use-case ├── Cargo.toml ├── README.md ├── default.nix └── src ├── answer_form.rs ├── answer_registration_form.rs ├── assign_user_role_to_email.rs ├── create_file.rs ├── create_form.rs ├── create_project.rs ├── create_registration_form.rs ├── delete_user_invitation.rs ├── distribute_files.rs ├── error.rs ├── export_form_answers.rs ├── export_projects.rs ├── export_registration_form_answers.rs ├── export_users.rs ├── get_distributed_file.rs ├── get_file.rs ├── get_file_distribution.rs ├── get_file_object.rs ├── get_file_sharing.rs ├── get_form.rs ├── get_form_answer.rs ├── get_form_answer_shared_file.rs ├── get_form_answer_shared_file_object.rs ├── get_login_user.rs ├── get_pending_project.rs ├── get_pending_project_registration_form.rs ├── get_pending_project_registration_form_answer.rs ├── get_project.rs ├── get_project_by_code.rs ├── get_project_creation_availability.rs ├── get_project_form.rs ├── get_project_form_answer.rs ├── get_project_form_answer_shared_file.rs ├── get_project_form_answer_shared_file_object.rs ├── get_project_registration_form.rs ├── get_project_registration_form_answer.rs ├── get_project_registration_form_answer_shared_file.rs ├── get_project_registration_form_answer_shared_file_object.rs ├── get_project_shared_file.rs ├── get_project_shared_file_object.rs ├── get_publicly_shared_file.rs ├── get_publicly_shared_file_object.rs ├── get_registration_form.rs ├── get_registration_form_answer.rs ├── get_registration_form_answer_shared_file.rs ├── get_registration_form_answer_shared_file_object.rs ├── get_shared_file.rs ├── get_shared_file_object.rs ├── get_user.rs ├── get_user_file_usage.rs ├── get_user_invitation.rs ├── get_user_pending_project.rs ├── get_user_project.rs ├── interface.rs ├── interface ├── form.rs ├── form │ ├── check_answer_error.rs │ ├── condition.rs │ └── item.rs ├── form_answer.rs ├── form_answer │ └── item.rs └── project_query.rs ├── invite_user.rs ├── lib.rs ├── list_all_file_distributions.rs ├── list_all_forms.rs ├── list_all_projects.rs ├── list_all_registration_forms.rs ├── list_all_user_invitations.rs ├── list_distributed_files.rs ├── list_form_answers.rs ├── list_pending_project_registration_forms.rs ├── list_project_forms.rs ├── list_project_registration_forms.rs ├── list_registration_form_answers.rs ├── list_user_file_sharings.rs ├── list_user_files.rs ├── list_users.rs ├── model.rs ├── model ├── file.rs ├── file_distribution.rs ├── file_sharing.rs ├── form.rs ├── form │ ├── item.rs │ └── item │ │ ├── checkbox.rs │ │ ├── condition.rs │ │ ├── grid_radio.rs │ │ └── radio.rs ├── form_answer.rs ├── form_answer │ └── item.rs ├── pending_project.rs ├── project.rs ├── project_creation_availability.rs ├── project_query.rs ├── registration_form.rs ├── registration_form_answer.rs ├── stream.rs ├── user.rs └── user_invitation.rs ├── prepare_project.rs ├── revoke_file_sharing.rs ├── share_file.rs ├── signup.rs ├── test.rs ├── test ├── interface.rs └── interface │ └── form_answer.rs ├── update_any_pending_project.rs ├── update_any_project.rs ├── update_any_user.rs ├── update_form.rs ├── update_pending_project.rs ├── update_pending_project_registration_form_answer.rs ├── update_project.rs ├── update_project_form_answer.rs └── update_project_registration_form_answer.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.envrc.sample: -------------------------------------------------------------------------------- 1 | export POSTGRES_PASSWORD=sos21 2 | export POSTGRES_USER=sos21 3 | export POSTGRES_DB=sos21 4 | export POSTGRES_PORT=5432 5 | 6 | export MINIO_ROOT_USER=sos21 7 | export MINIO_ROOT_PASSWORD=sos21sos21 8 | export MINIO_PORT=9000 9 | 10 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DB}" 11 | 12 | export SOS21_API_SERVER_JWT_AUDIENCE= 13 | export SOS21_API_SERVER_JWT_ISSUER="https://securetoken.google.com/$SOS21_API_SERVER_JWT_AUDIENCE" 14 | export SOS21_API_SERVER_JWT_KEYS_URL="https://www.googleapis.com/robot/v1/metadata/jwk/securetoken@system.gserviceaccount.com" 15 | export SOS21_API_SERVER_POSTGRES_URI=$DATABASE_URL 16 | export SOS21_API_SERVER_S3_ACCESS_KEY=$MINIO_ROOT_USER 17 | export SOS21_API_SERVER_S3_ACCESS_SECRET=$MINIO_ROOT_PASSWORD 18 | export SOS21_API_SERVER_S3_REGION= 19 | export SOS21_API_SERVER_S3_ENDPOINT="http://localhost:${MINIO_PORT}" 20 | export SOS21_API_SERVER_S3_OBJECT_BUCKET=object 21 | export SOS21_API_SERVER_ADMINISTRATOR_EMAIL= 22 | export SOS21_API_SERVER_BIND=127.0.0.1:3000 23 | export SOS21_API_SERVER_ADMIN_REPORT_SLACK_WEBHOOK= 24 | 25 | export RUST_BACKTRACE=1 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.nix linguist-generated 2 | Cargo.lock linguist-generated 3 | sos21-database/sqlx-data.json linguist-generated 4 | sos21-api-server/schema/package-lock.json linguist-generated 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @momeemt 2 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | - cron: '0 0 * * *' 4 | 5 | name: Audit 6 | 7 | jobs: 8 | audit: 9 | name: Security audit 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: cachix/install-nix-action@v20 14 | - uses: cachix/cachix-action@v12 15 | with: 16 | name: sos-backend 17 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN_JSYS23 }}" 18 | # workaround for actions-rs/audit-check#163 19 | - run: nix-env -if ./nix/ignore-generate-lockfile-cargo.nix 20 | - uses: actions-rs/audit-check@v1 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/generate-documentation.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - develop 5 | 6 | name: Generate documentation 7 | 8 | jobs: 9 | api-server: 10 | name: Generate documentation from OpenAPI schema 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: cachix/install-nix-action@v20 15 | - run: nix-env -f ./nix/pkgs.nix -iA nodejs 16 | - run: cd sos21-api-server/schema; npm install 17 | - run: cd sos21-api-server/schema; npm run build:doc -- --output dist/api-server.html 18 | - uses: peaceiris/actions-gh-pages@v3 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | publish_dir: ./sos21-api-server/schema/dist 22 | destination_dir: develop 23 | keep_files: true 24 | rustdoc: 25 | name: Generate rustdoc documentation 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: cachix/install-nix-action@v20 30 | - uses: cachix/cachix-action@v12 31 | with: 32 | name: sos-backend 33 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN_JSYS23 }}' 34 | - run: nix-env -f ./nix/pkgs.nix -iA rustPlatform.rust.cargo 35 | - uses: actions-rs/cargo@v1 36 | with: 37 | command: doc 38 | args: --no-deps --document-private-items 39 | - uses: peaceiris/actions-gh-pages@v3 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | publish_dir: ./target/doc 43 | destination_dir: develop 44 | keep_files: true 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | /result 4 | /result-* 5 | node_modules 6 | .envrc 7 | .cargo 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "sos21-domain", 4 | "sos21-use-case", 5 | "sos21-database", 6 | "sos21-gateway/database", 7 | "sos21-gateway/s3", 8 | "sos21-gateway/slack", 9 | "sos21-run-migrations", 10 | "sos21-api-server", 11 | ] 12 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.1 2 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ./nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | # sos21-database (that uses sqlx) needs cargo during compilation 6 | cargoMetadataNoDeps = pkgs.writeShellScript "cargo-wrapped" '' 7 | [ "$1" != "metadata" ] && exit 1 8 | shift 9 | ${pkgs.cargo}/bin/cargo metadata "$@" --no-deps 10 | ''; 11 | cargoNix = pkgs.callPackage ./Cargo.nix { 12 | defaultCrateOverrides = pkgs.defaultCrateOverrides // { 13 | sos21-database = oldAttrs: { 14 | CARGO = "${cargoMetadataNoDeps}"; 15 | }; 16 | sos21-api-server = oldAttrs: { 17 | src = pkgs.symlinkJoin { 18 | name = "${oldAttrs.crateName}-src"; 19 | paths = [ oldAttrs.src ]; 20 | postBuild = '' 21 | cp -r ${builtins.path { 22 | name = "git"; 23 | path = ./.git; 24 | }} $out/.git 25 | ''; 26 | }; 27 | }; 28 | }; 29 | }; 30 | in 31 | pkgs.lib.mapAttrs 32 | (_: crate: crate.build.override { 33 | inherit runTests; 34 | }) 35 | cargoNix.workspaceMembers 36 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | prism: 5 | image: stoplight/prism:4 6 | command: proxy /schema/api.yml http://${SOS21_API_SERVER_BIND:?} --errors 7 | volumes: 8 | - ./sos21-api-server/schema:/schema 9 | network_mode: host 10 | db: 11 | image: postgres:13.1-alpine 12 | environment: 13 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?} 14 | POSTGRES_USER: ${POSTGRES_USER:?} 15 | POSTGRES_DB: ${POSTGRES_DB:?} 16 | ports: 17 | - ${POSTGRES_PORT:?}:5432 18 | volumes: 19 | - postgres-data-dev:/var/lib/postgresql/data 20 | minio: 21 | image: minio/minio:RELEASE.2021-03-12T00-00-47Z 22 | command: server /data 23 | environment: 24 | MINIO_ROOT_USER: ${MINIO_ROOT_USER:?} 25 | MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?} 26 | ports: 27 | - ${MINIO_PORT:?}:9000 28 | volumes: 29 | - minio-data-dev:/data 30 | create-bucket: 31 | image: minio/mc:RELEASE.2021-03-12T03-36-59Z 32 | command: mb -p minio/${SOS21_API_SERVER_S3_OBJECT_BUCKET} 33 | environment: 34 | MC_HOST_minio: "http://${MINIO_ROOT_USER:?}:${MINIO_ROOT_PASSWORD:?}@minio:${MINIO_PORT:?}" 35 | depends_on: 36 | - minio 37 | 38 | volumes: 39 | postgres-data-dev: 40 | minio-data-dev: 41 | -------------------------------------------------------------------------------- /nix/crate2nix.nix: -------------------------------------------------------------------------------- 1 | import (builtins.fetchTarball { 2 | url = "https://github.com/kolloch/crate2nix/tarball/0.10.0"; 3 | sha256 = "11kvyqd79d4pr4s5qb9h9db0l21qckip0mybwkf6mqg6gpy7v895"; 4 | }) 5 | -------------------------------------------------------------------------------- /nix/deployment/acme.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | { 3 | security.acme = { 4 | acceptTerms = true; 5 | email = "info@sohosai.com"; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /nix/deployment/beta.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | let 3 | apiServer = "http://localhost:${toString config.services.sos21-api-server.port}"; 4 | in 5 | { 6 | imports = [ 7 | ./sos21-api-server.nix 8 | ./nginx.nix 9 | ./acme.nix 10 | ./staging.nix 11 | ]; 12 | 13 | services.nginx = { 14 | virtualHosts."api.beta.online.sohosai.com" = { 15 | enableACME = true; 16 | forceSSL = true; 17 | locations."/" = { 18 | proxyPass = apiServer; 19 | }; 20 | locations."/file/create" = { 21 | proxyPass = "${apiServer}/file/create"; 22 | extraConfig = '' 23 | proxy_request_buffering off; 24 | client_max_body_size 0; 25 | ''; 26 | }; 27 | }; 28 | }; 29 | 30 | services.sos21-api-server = { 31 | enable = true; 32 | port = 3000; 33 | firebaseProjectId = "sos23-beta"; 34 | databaseName = "sos21-beta"; 35 | s3ObjectBucket = "sos21-beta-objects"; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /nix/deployment/main.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | let 3 | apiServer = "http://localhost:${toString config.services.sos21-api-server.port}"; 4 | in 5 | { 6 | imports = [ 7 | ./sos21-api-server.nix 8 | ./nginx.nix 9 | ./acme.nix 10 | ./production.nix 11 | ]; 12 | 13 | services.nginx = { 14 | clientMaxBodySize = "2m"; 15 | 16 | virtualHosts."api.online.sohosai.com" = { 17 | enableACME = true; 18 | forceSSL = true; 19 | locations."/" = { 20 | proxyPass = apiServer; 21 | }; 22 | locations."/file/create" = { 23 | proxyPass = "${apiServer}/file/create"; 24 | extraConfig = '' 25 | proxy_request_buffering off; 26 | client_max_body_size 0; 27 | ''; 28 | }; 29 | }; 30 | }; 31 | 32 | services.sos21-api-server = { 33 | enable = true; 34 | port = 3000; 35 | firebaseProjectId = "sos23-main"; 36 | databaseName = "sos21-main"; 37 | s3ObjectBucket = "sos21-main-objects"; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /nix/deployment/nginx.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | { 3 | networking.firewall.allowedTCPPorts = [ 80 443 ]; 4 | 5 | services.nginx = { 6 | enable = true; 7 | 8 | recommendedProxySettings = true; 9 | recommendedTlsSettings = true; 10 | recommendedGzipSettings = true; 11 | recommendedOptimisation = true; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /nix/deployment/nightly.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | let 3 | apiServer = "http://localhost:${toString config.services.sos21-api-server.port}"; 4 | in 5 | { 6 | imports = [ 7 | ./sos21-api-server.nix 8 | ./nginx.nix 9 | ./acme.nix 10 | ./staging.nix 11 | ]; 12 | 13 | services.nginx = { 14 | virtualHosts."api.nightly.online.sohosai.com" = { 15 | enableACME = true; 16 | forceSSL = true; 17 | locations."/" = { 18 | proxyPass = apiServer; 19 | }; 20 | locations."/file/create" = { 21 | proxyPass = "${apiServer}/file/create"; 22 | extraConfig = '' 23 | proxy_request_buffering off; 24 | client_max_body_size 0; 25 | ''; 26 | }; 27 | }; 28 | }; 29 | 30 | services.sos21-api-server = { 31 | enable = true; 32 | port = 3000; 33 | firebaseProjectId = "sos23-nightly"; 34 | databaseName = "sos21-nightly"; 35 | s3ObjectBucket = "sos21-nightly-objects"; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /nix/deployment/production.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | { 3 | imports = [ 4 | ./sos21-api-server.nix 5 | ]; 6 | 7 | # TODO: Inject these constants from sos-backend-infrastructure 8 | services.sos21-api-server = { 9 | databaseHost = "192.168.0.11"; 10 | databasePort = 5432; 11 | databaseUsernameFile = /var/keys/database-username; 12 | databasePasswordFile = /var/keys/database-password; 13 | s3Endpoint = "http://192.168.0.12:9000"; 14 | s3AccessKeyFile = /var/keys/minio-access-key; 15 | s3AccessSecretFile = /var/keys/minio-secret-key; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /nix/deployment/staging.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | { 3 | imports = [ 4 | ./sos21-api-server.nix 5 | ]; 6 | 7 | # TODO: Inject these constants from sos-backend-infrastructure 8 | services.sos21-api-server = { 9 | databaseHost = "192.168.0.11"; 10 | databasePort = 5432; 11 | databaseUsernameFile = /var/keys/database-username; 12 | databasePasswordFile = /var/keys/database-password; 13 | s3Endpoint = "http://192.168.0.12:9000"; 14 | s3AccessKeyFile = /var/keys/minio-access-key; 15 | s3AccessSecretFile = /var/keys/minio-secret-key; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /nix/ignore-generate-lockfile-cargo.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ./pkgs.nix }: 2 | pkgs.writeShellScriptBin "cargo" '' 3 | if [ "$1" == "generate-lockfile" ]; then 4 | 1>&2 echo 'Ignoring `cargo generate-lockfile`' 5 | exit 0 6 | fi 7 | ${pkgs.cargo}/bin/cargo "$@" 8 | '' 9 | -------------------------------------------------------------------------------- /nix/image.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ./pkgs.nix 2 | , name ? "sos21-backend" 3 | , tag ? null 4 | }: 5 | let 6 | sos21-backend = import ../. { inherit pkgs; }; 7 | inherit (sos21-backend) sos21-api-server sos21-run-migrations; 8 | in 9 | pkgs.dockerTools.buildImage { 10 | inherit name tag; 11 | contents = [ 12 | sos21-api-server 13 | sos21-run-migrations 14 | # hyper-rustls under rusoto needs the native CA certificates (rusoto/rusoto#1811) 15 | pkgs.cacert 16 | ]; 17 | config = { 18 | Cmd = [ "/bin/sos21-api-server" ]; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /nix/overlay.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | let 3 | rustPlatform = super.callPackage ./rustPlatform.nix { }; 4 | in 5 | { 6 | inherit rustPlatform; 7 | inherit (rustPlatform.rust) rustc cargo; 8 | } 9 | -------------------------------------------------------------------------------- /nix/pkgs.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/22.11.tar.gz"; 4 | sha256 = "11w3wn2yjhaa5pv20gbfbirvjq6i3m7pqrq2msf0g7cv44vijwgw"; 5 | }; 6 | moz_overlay = builtins.fetchTarball { 7 | url = "https://github.com/mozilla/nixpkgs-mozilla/archive/78e723925daf5c9e8d0a1837ec27059e61649cb6.tar.gz"; 8 | sha256 = "0k3jxk21s28jsfpmqv39vyhfz2srfm81kp4xnpzgsbjn77rhwn03"; 9 | }; 10 | in 11 | import nixpkgs { 12 | overlays = [ 13 | (import moz_overlay) 14 | (import ./overlay.nix) 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /nix/rustPlatform.nix: -------------------------------------------------------------------------------- 1 | { rustChannelOf, makeRustPlatform }: 2 | let 3 | channel = rustChannelOf { 4 | channel = "1.69.0"; 5 | sha256 = "eMJethw5ZLrJHmoN2/l0bIyQjoTX1NsvalWSscTixpI="; 6 | }; 7 | in 8 | makeRustPlatform { 9 | rustc = channel.rust; 10 | cargo = channel.rust; 11 | } 12 | -------------------------------------------------------------------------------- /nix/sqlx-cli.nix: -------------------------------------------------------------------------------- 1 | { rustPlatform, fetchFromGitHub, openssl, pkg-config, lib }: 2 | rustPlatform.buildRustPackage rec { 3 | pname = "sqlx-cli"; 4 | version = "0.5.10"; 5 | src = builtins.fetchTarball { 6 | url = "https://github.com/launchbadge/sqlx/archive/v0.5.10.tar.gz"; 7 | sha256 = "0d0yv9hvd73cl5h4fc061ghhvk3c8kgii4sjxwngq2zl318jw4iq"; 8 | }; 9 | nativeBuildInputs = [ pkg-config ]; 10 | buildInputs = [ openssl ]; 11 | cargoBuildFlags = [ "--manifest-path=sqlx-cli/Cargo.toml" ]; 12 | cargoSha256 = "9+I4mi7w1WK2NkmN65EtC52KtSZR9GjrHCPE9w82IXw="; 13 | doCheck = false; 14 | meta = with lib; { 15 | description = "Command-line utility for SQLx, the Rust SQL toolkit."; 16 | license = licenses.mit; 17 | platforms = platforms.all; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /script/flush_changelog.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell -i bash --pure 3 | # shellcheck shell=bash 4 | 5 | # flush the changelogs in the project. 6 | # 7 | # usage: 8 | # ./script/flush_changelog.sh 9 | 10 | set -feu -o pipefail 11 | 12 | PROJECT=$(git rev-parse --show-toplevel) 13 | readonly PROJECT=${PROJECT%/} 14 | readonly SCRIPT=$0 15 | 16 | TEMPLATE=$(cat <<'EOT' 17 | ## [Unreleased] 18 | 19 | ### Added 20 | ### Changed 21 | ### Deprecated 22 | ### Removed 23 | ### Fixed 24 | ### Security 25 | EOT 26 | ) 27 | readonly TEMPLATE 28 | 29 | function main() { 30 | set -feu -o pipefail 31 | 32 | if [ $# != 1 ]; then 33 | 1>&2 echo "usage: $SCRIPT " 34 | return 1 35 | fi 36 | 37 | local -r version=$1 38 | local date 39 | date=$(date +'%Y-%m-%d') 40 | readonly date 41 | 42 | 1>&2 echo "version: $version" 43 | 1>&2 echo "date: $date" 44 | 45 | local -a files 46 | IFS=$'\n' read -r -d '' -a files < <(git ls-files | grep 'CHANGELOG.md$'; printf '\0') 47 | readonly files 48 | 49 | if [ ${#files[*]} -eq 0 ]; then 50 | 1>&2 echo "no CHANGELOG.md found" 51 | return 1 52 | fi 53 | 54 | for file in "${files[@]}"; do 55 | sed -i -e '/^###/{:b;N;/### \w*$/{D;bb}}' -e '/^###/{:c;N;/\n$/bc;s/.*\n##/##/}' "$file" 56 | sed -i -e 's/^## \[Unreleased\]$/'"${TEMPLATE//$'\n'/'\n'}\n\n## [$version] - $date/" "$file" 57 | done 58 | 59 | { echo "updated:"; printf '%s\n' "${files[@]}"; } 1>&2 60 | } 61 | 62 | main "$@" 63 | -------------------------------------------------------------------------------- /script/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix }: 2 | let 3 | crate2nix = import ../nix/crate2nix.nix { }; 4 | in 5 | pkgs.mkShell { 6 | nativeBuildInputs = with pkgs; [ 7 | cargo 8 | coreutils 9 | crate2nix 10 | findutils 11 | gitMinimal 12 | gnused 13 | jq 14 | nodejs 15 | yq-go 16 | ]; 17 | 18 | # Do not search $HOME/.cargo/bin for subcommands (rust-lang/cargo#6507) 19 | CARGO_HOME = toString ../.cargo; 20 | } 21 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ./nix/pkgs.nix }: 2 | let 3 | sqlx-cli = pkgs.callPackage ./nix/sqlx-cli.nix { }; 4 | crate2nix = import ./nix/crate2nix.nix { }; 5 | in 6 | pkgs.mkShell { 7 | nativeBuildInputs = with pkgs; [ 8 | zlib 9 | rustc 10 | cargo 11 | sqlx-cli 12 | crate2nix 13 | openssl 14 | pkgconfig 15 | ]; 16 | 17 | # Do not search $HOME/.cargo/bin for subcommands (rust-lang/cargo#6507) 18 | CARGO_HOME = toString ./.cargo; 19 | } 20 | -------------------------------------------------------------------------------- /sos21-api-server/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-api-server` 2 | 3 | - [API server documentation (develop)](https://sohosai.github.io/sos21-backend/develop/api-server.html) 4 | - [API server documentation](https://sohosai.github.io/sos21-backend/api-server.html) 5 | 6 | インフラ層。リクエストに応じて `sos21-use-case` のビジネスロジックを呼び出す HTTP サーバーです。 7 | -------------------------------------------------------------------------------- /sos21-api-server/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | vergen::vergen(vergen::Config::default())?; 3 | 4 | Ok(()) 5 | } 6 | -------------------------------------------------------------------------------- /sos21-api-server/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-api-server 8 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/DateTime.yml: -------------------------------------------------------------------------------- 1 | type: number 2 | title: DateTime 3 | description: January 1, 1970 00:00:00 UTC からの経過ミリ秒で表現された単一の瞬間の時刻 4 | x-examples: {} 5 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/DistributedFile.yml: -------------------------------------------------------------------------------- 1 | title: DistributedFile 2 | type: object 3 | properties: 4 | distribution_id: 5 | $ref: ./file_distribution/FileDistributionId.yml 6 | distributed_at: 7 | $ref: ./DateTime.yml 8 | name: 9 | type: string 10 | description: 11 | type: string 12 | project_id: 13 | $ref: ./project/ProjectId.yml 14 | sharing_id: 15 | $ref: ./file_sharing/FileSharingId.yml 16 | required: 17 | - distribution_id 18 | - distributed_at 19 | - name 20 | - description 21 | - project_id 22 | - sharing_id 23 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/Mime.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: Mime 3 | format: mime 4 | description: RFC7231 の Media Type です。 5 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/Null.yml: -------------------------------------------------------------------------------- 1 | title: "Null" 2 | description: | 3 | This only accepts `null`. 4 | Workaround for . 5 | nullable: true 6 | not: 7 | anyOf: 8 | - type: string 9 | - type: number 10 | - type: boolean 11 | - type: object 12 | - type: array 13 | items: {} 14 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/ProjectQuery.yml: -------------------------------------------------------------------------------- 1 | title: ProjectQuery 2 | type: array 3 | x-examples: {} 4 | items: 5 | type: object 6 | properties: 7 | category: 8 | oneOf: 9 | - $ref: ./Null.yml 10 | - $ref: ./project/ProjectCategory.yml 11 | attributes: 12 | type: array 13 | items: 14 | $ref: ./project/ProjectAttribute.yml 15 | required: 16 | - category 17 | - attributes 18 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/error/Error.yml: -------------------------------------------------------------------------------- 1 | title: Error 2 | type: object 3 | x-examples: {} 4 | properties: 5 | status: 6 | type: integer 7 | error: 8 | $ref: ./ErrorBody.yml 9 | required: 10 | - status 11 | - error 12 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/error/ErrorBody.yml: -------------------------------------------------------------------------------- 1 | title: ErrorBody 2 | oneOf: 3 | - properties: 4 | type: 5 | type: string 6 | enum: 7 | - API 8 | info: {} 9 | required: 10 | - type 11 | - info 12 | - properties: 13 | type: 14 | type: string 15 | enum: 16 | - AUTHENTICATION 17 | id: 18 | type: string 19 | enum: 20 | - UNAUTHORIZED 21 | - INVALID_TOKEN 22 | - INVALID_EMAIL_ADDRESS 23 | - UNVERIFIED_EMAIL_ADDRESS 24 | - NOT_UNIVERSITY_EMAIL_ADDRESS 25 | - NO_EMAIL 26 | required: 27 | - type 28 | - id 29 | - properties: 30 | type: 31 | type: string 32 | enum: 33 | - REQUEST 34 | id: 35 | type: string 36 | enum: 37 | - NOT_FOUND 38 | - UNSUPPORTED_MEDIA_TYPE 39 | - MISSING_HEADER 40 | - METHOD_NOT_ALLOWED 41 | - INVALID_HEADER 42 | - INVALID_QUERY 43 | - INVALID_BODY 44 | - CORS_FORBIDDEN 45 | required: 46 | - type 47 | - id 48 | - properties: 49 | type: 50 | type: string 51 | enum: 52 | - NOT_SIGNED_UP 53 | - INTERNAL 54 | - SERVICE_UNAVAILABLE 55 | required: 56 | - type 57 | x-examples: {} 58 | description: "" 59 | type: object 60 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file/File.yml: -------------------------------------------------------------------------------- 1 | title: File 2 | type: object 3 | description: "" 4 | properties: 5 | id: 6 | $ref: ./FileId.yml 7 | created_at: 8 | $ref: ../DateTime.yml 9 | author_id: 10 | $ref: ../user/UserId.yml 11 | blake3_digest: 12 | type: string 13 | name: 14 | type: string 15 | nullable: true 16 | type: 17 | $ref: ../Mime.yml 18 | size: 19 | type: integer 20 | required: 21 | - id 22 | - created_at 23 | - author_id 24 | - blake3_digest 25 | - name 26 | - type 27 | - size 28 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file/FileId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: FileId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file_distribution/FileDistribution.yml: -------------------------------------------------------------------------------- 1 | title: FileDistribution 2 | type: object 3 | properties: 4 | id: 5 | $ref: ./FileDistributionId.yml 6 | created_at: 7 | $ref: ../DateTime.yml 8 | author_id: 9 | $ref: ../user/UserId.yml 10 | name: 11 | type: string 12 | description: 13 | type: string 14 | files: 15 | type: array 16 | items: 17 | type: object 18 | properties: 19 | project_id: 20 | $ref: ../project/ProjectId.yml 21 | sharing_id: 22 | $ref: ../file_sharing/FileSharingId.yml 23 | required: 24 | - project_id 25 | - sharing_id 26 | required: 27 | - id 28 | - created_at 29 | - author_id 30 | - name 31 | - description 32 | - files 33 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file_distribution/FileDistributionId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: FileDistributionId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file_sharing/FileSharing.yml: -------------------------------------------------------------------------------- 1 | title: FileSharing 2 | type: object 3 | description: "" 4 | properties: 5 | id: 6 | $ref: ./FileSharingId.yml 7 | created_at: 8 | $ref: ../DateTime.yml 9 | is_revoked: 10 | type: boolean 11 | expires_at: 12 | oneOf: 13 | - $ref: ../DateTime.yml 14 | - $ref: ../Null.yml 15 | scope: 16 | $ref: ./FileSharingScope.yml 17 | file_id: 18 | $ref: ../file/FileId.yml 19 | file_name: 20 | type: string 21 | nullable: true 22 | file_type: 23 | $ref: ../Mime.yml 24 | file_size: 25 | type: integer 26 | required: 27 | - id 28 | - created_at 29 | - is_revoked 30 | - expires_at 31 | - scope 32 | - file_id 33 | - file_name 34 | - file_type 35 | - file_size 36 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file_sharing/FileSharingId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: FileSharingId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/file_sharing/FileSharingScope.yml: -------------------------------------------------------------------------------- 1 | title: FileSharingScope 2 | oneOf: 3 | - type: object 4 | properties: 5 | type: 6 | type: string 7 | enum: 8 | - project 9 | id: 10 | $ref: ../project/ProjectId.yml 11 | required: 12 | - type 13 | - id 14 | - type: object 15 | properties: 16 | type: 17 | type: string 18 | enum: 19 | - project_query 20 | query: 21 | $ref: ../ProjectQuery.yml 22 | required: 23 | - type 24 | - query 25 | - type: object 26 | properties: 27 | type: 28 | type: string 29 | enum: 30 | - form_answer 31 | project_id: 32 | $ref: ../project/ProjectId.yml 33 | form_id: 34 | $ref: ../form/FormId.yml 35 | required: 36 | - type 37 | - project_id 38 | - form_id 39 | - type: object 40 | properties: 41 | type: 42 | type: string 43 | enum: 44 | - registration_form_answer 45 | project_id: 46 | $ref: ../project/ProjectId.yml 47 | pending_project_id: 48 | $ref: ../pending_project/PendingProjectId.yml 49 | form_id: 50 | $ref: ../registration_form/RegistrationFormId.yml 51 | required: 52 | - type 53 | - registration_form_id 54 | - type: object 55 | properties: 56 | type: 57 | type: string 58 | enum: 59 | - committee 60 | - committee_operator 61 | - public 62 | required: 63 | - type 64 | description: "" 65 | x-examples: {} 66 | type: object 67 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/Form.yml: -------------------------------------------------------------------------------- 1 | title: Form 2 | type: object 3 | x-examples: {} 4 | properties: 5 | id: 6 | $ref: ./FormId.yml 7 | created_at: 8 | $ref: ../DateTime.yml 9 | author_id: 10 | $ref: ../user/UserId.yml 11 | name: 12 | type: string 13 | description: 14 | type: string 15 | starts_at: 16 | $ref: ../DateTime.yml 17 | ends_at: 18 | $ref: ../DateTime.yml 19 | items: 20 | type: array 21 | items: 22 | $ref: ./item/FormItem.yml 23 | condition: 24 | $ref: ./FormCondition.yml 25 | required: 26 | - id 27 | - created_at 28 | - author_id 29 | - name 30 | - description 31 | - starts_at 32 | - ends_at 33 | - items 34 | - condition 35 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/FormCondition.yml: -------------------------------------------------------------------------------- 1 | title: FormCondition 2 | type: object 3 | properties: 4 | query: 5 | $ref: ../ProjectQuery.yml 6 | includes: 7 | type: array 8 | items: 9 | $ref: ../project/ProjectId.yml 10 | excludes: 11 | type: array 12 | items: 13 | $ref: ../project/ProjectId.yml 14 | required: 15 | - query 16 | - includes 17 | - excludes 18 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/FormId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: FormId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/item/CheckboxId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: CheckboxId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/item/FormItemCondition.yml: -------------------------------------------------------------------------------- 1 | title: FormItemCondition 2 | oneOf: 3 | - properties: 4 | type: 5 | type: string 6 | enum: 7 | - checkbox 8 | item_id: 9 | $ref: ./FormItemId.yml 10 | checkbox_id: 11 | $ref: ./CheckboxId.yml 12 | expected: 13 | type: boolean 14 | required: 15 | - type 16 | - item_id 17 | - checkbox_id 18 | - expected 19 | - properties: 20 | type: 21 | type: string 22 | enum: 23 | - radio_selected 24 | item_id: 25 | $ref: ./FormItemId.yml 26 | radio_id: 27 | $ref: ./RadioId.yml 28 | required: 29 | - type 30 | - item_id 31 | - radio_id 32 | - properties: 33 | type: 34 | type: string 35 | enum: 36 | - grid_radio_selected 37 | item_id: 38 | $ref: ./FormItemId.yml 39 | column_id: 40 | $ref: ./GridRadioColumnId.yml 41 | required: 42 | - type 43 | - item_id 44 | - column_id 45 | x-examples: {} 46 | type: object 47 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/item/FormItemId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: FormItemId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/item/GridRadioColumnId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: GridRadioColumnId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/item/GridRadioRowId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: GridRadioRowId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form/item/RadioId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: RadioId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form_answer/FormAnswer.yml: -------------------------------------------------------------------------------- 1 | title: FormAnswer 2 | type: object 3 | x-examples: {} 4 | properties: 5 | id: 6 | $ref: ./FormAnswerId.yml 7 | project_id: 8 | $ref: ../project/ProjectId.yml 9 | form_id: 10 | $ref: ../form/FormId.yml 11 | created_at: 12 | $ref: ../DateTime.yml 13 | author_id: 14 | $ref: ../user/UserId.yml 15 | items: 16 | type: array 17 | items: 18 | $ref: ./FormAnswerItem.yml 19 | required: 20 | - id 21 | - project_id 22 | - form_id 23 | - created_at 24 | - author_id 25 | - items 26 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/form_answer/FormAnswerId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: FormAnswerId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/pending_project/PendingProject.yml: -------------------------------------------------------------------------------- 1 | title: PendingProject 2 | type: object 3 | properties: 4 | id: 5 | $ref: ./PendingProjectId.yml 6 | created_at: 7 | $ref: ../DateTime.yml 8 | updated_at: 9 | $ref: ../DateTime.yml 10 | owner_id: 11 | $ref: ../user/UserId.yml 12 | name: 13 | type: string 14 | kana_name: 15 | type: string 16 | group_name: 17 | type: string 18 | kana_group_name: 19 | type: string 20 | description: 21 | type: string 22 | category: 23 | $ref: ../project/ProjectCategory.yml 24 | attributes: 25 | type: array 26 | items: 27 | $ref: ../project/ProjectAttribute.yml 28 | exceptional_complete_deadline: 29 | $ref: ../DateTime.yml 30 | required: 31 | - id 32 | - created_at 33 | - updated_at 34 | - owner_id 35 | - name 36 | - kana_name 37 | - group_name 38 | - kana_group_name 39 | - description 40 | - category 41 | - attributes 42 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/pending_project/PendingProjectId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: PendingProjectId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/project/Project.yml: -------------------------------------------------------------------------------- 1 | title: Project 2 | type: object 3 | description: "" 4 | x-examples: {} 5 | properties: 6 | id: 7 | $ref: ./ProjectId.yml 8 | code: 9 | type: string 10 | created_at: 11 | $ref: ../DateTime.yml 12 | updated_at: 13 | $ref: ../DateTime.yml 14 | owner_id: 15 | $ref: ../user/UserId.yml 16 | owner_name: 17 | $ref: ../user/UserName.yml 18 | owner_kana_name: 19 | $ref: ../user/UserKanaName.yml 20 | subowner_id: 21 | $ref: ../user/UserId.yml 22 | subowner_name: 23 | $ref: ../user/UserName.yml 24 | subowner_kana_name: 25 | $ref: ../user/UserKanaName.yml 26 | name: 27 | type: string 28 | kana_name: 29 | type: string 30 | group_name: 31 | type: string 32 | kana_group_name: 33 | type: string 34 | description: 35 | type: string 36 | category: 37 | $ref: ./ProjectCategory.yml 38 | attributes: 39 | type: array 40 | items: 41 | $ref: ./ProjectAttribute.yml 42 | required: 43 | - id 44 | - code 45 | - created_at 46 | - updated_at 47 | - owner_id 48 | - owner_name 49 | - owner_kana_name 50 | - subowner_id 51 | - subowner_name 52 | - subowner_kana_name 53 | - name 54 | - kana_name 55 | - group_name 56 | - kana_group_name 57 | - description 58 | - category 59 | - attributes 60 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/project/ProjectAttribute.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: ProjectAttribute 3 | enum: 4 | - academic 5 | - artistic 6 | - committee 7 | - outdoor 8 | - indoor 9 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/project/ProjectCategory.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: ProjectCategory 3 | enum: 4 | - general 5 | - cooking_requiring_preparation_area 6 | - cooking 7 | - food 8 | - stage 9 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/project/ProjectId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: ProjectId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/registration_form/RegistrationForm.yml: -------------------------------------------------------------------------------- 1 | title: RegistrationForm 2 | type: object 3 | x-examples: {} 4 | properties: 5 | id: 6 | $ref: ./RegistrationFormId.yml 7 | created_at: 8 | $ref: ../DateTime.yml 9 | author_id: 10 | $ref: ../user/UserId.yml 11 | name: 12 | type: string 13 | description: 14 | type: string 15 | items: 16 | type: array 17 | items: 18 | $ref: ../form/item/FormItem.yml 19 | query: 20 | $ref: ../ProjectQuery.yml 21 | required: 22 | - id 23 | - created_at 24 | - author_id 25 | - name 26 | - description 27 | - items 28 | - query 29 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/registration_form/RegistrationFormId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: RegistrationFormId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/registration_form_answer/RegistrationFormAnswer.yml: -------------------------------------------------------------------------------- 1 | title: RegistrationFormAnswer 2 | type: object 3 | properties: 4 | id: 5 | $ref: ./RegistrationFormAnswerId.yml 6 | registration_form_id: 7 | $ref: ../registration_form/RegistrationFormId.yml 8 | created_at: 9 | $ref: ../DateTime.yml 10 | updated_at: 11 | $ref: ../DateTime.yml 12 | author_id: 13 | $ref: ../user/UserId.yml 14 | project_id: 15 | $ref: ../project/ProjectId.yml 16 | pending_project_id: 17 | $ref: ../pending_project/PendingProjectId.yml 18 | items: 19 | type: array 20 | items: 21 | $ref: ../form_answer/FormAnswerItem.yml 22 | required: 23 | - id 24 | - form_id 25 | - created_at 26 | - updated_at 27 | - author_id 28 | - items 29 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/registration_form_answer/RegistrationFormAnswerId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: RegistrationFormAnswerId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user/User.yml: -------------------------------------------------------------------------------- 1 | title: User 2 | type: object 3 | description: "" 4 | x-tags: 5 | - user 6 | properties: 7 | id: 8 | $ref: ./UserId.yml 9 | created_at: 10 | $ref: ../DateTime.yml 11 | name: 12 | $ref: ./UserName.yml 13 | kana_name: 14 | $ref: ./UserKanaName.yml 15 | email: 16 | type: string 17 | format: email 18 | phone_number: 19 | type: string 20 | role: 21 | $ref: ./UserRole.yml 22 | category: 23 | $ref: ./UserCategory.yml 24 | required: 25 | - id 26 | - created_at 27 | - name 28 | - kana_name 29 | - email 30 | - phone_number 31 | - role 32 | - category 33 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user/UserCategory.yml: -------------------------------------------------------------------------------- 1 | title: UserCategory 2 | type: string 3 | enum: 4 | - undergraduate_student 5 | - graduate_student 6 | - academic_staff 7 | example: undergraduate_student 8 | x-tags: 9 | - user 10 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user/UserId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: UserId 3 | x-tags: 4 | - user 5 | example: ZNNsHSUYKzhj8dKCocMOEGtkOwTI 6 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user/UserKanaName.yml: -------------------------------------------------------------------------------- 1 | title: UserKanaName 2 | type: object 3 | description: "" 4 | x-tags: 5 | - user 6 | properties: 7 | first: 8 | type: string 9 | example: たろう 10 | last: 11 | type: string 12 | example: そうほう 13 | required: 14 | - first 15 | - last 16 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user/UserName.yml: -------------------------------------------------------------------------------- 1 | title: UserName 2 | type: object 3 | description: "" 4 | x-tags: 5 | - user 6 | properties: 7 | first: 8 | type: string 9 | example: 太郎 10 | last: 11 | type: string 12 | example: 雙峰 13 | required: 14 | - first 15 | - last 16 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user/UserRole.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: UserRole 3 | enum: 4 | - administrator 5 | - committee_operator 6 | - committee 7 | - general 8 | description: "" 9 | x-tags: 10 | - user 11 | example: general 12 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user_invitation/UserInvitation.yml: -------------------------------------------------------------------------------- 1 | title: UserInvitation 2 | type: object 3 | x-tags: 4 | - user_invitation 5 | properties: 6 | id: 7 | $ref: ./UserInvitationId.yml 8 | created_at: 9 | $ref: ../DateTime.yml 10 | author_id: 11 | $ref: ../user/UserId.yml 12 | email: 13 | type: string 14 | format: email 15 | role: 16 | $ref: ./UserInvitationRole.yml 17 | required: 18 | - id 19 | - created_at 20 | - author_id 21 | - email 22 | - role 23 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user_invitation/UserInvitationId.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: UserInvitationId 3 | format: uuid 4 | -------------------------------------------------------------------------------- /sos21-api-server/schema/model/user_invitation/UserInvitationRole.yml: -------------------------------------------------------------------------------- 1 | type: string 2 | title: UserInvitationRole 3 | enum: 4 | - administrator 5 | - committee_operator 6 | - committee 7 | x-tags: 8 | - user_invitation 9 | example: committee 10 | -------------------------------------------------------------------------------- /sos21-api-server/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sos21-api-server-schema", 3 | "version": "0.7.1", 4 | "description": "OpenAPI schema for sos21-api-server", 5 | "scripts": { 6 | "lint:spectral": "spectral lint ./api.yml -F hint", 7 | "lint:prettier": "prettier --check .", 8 | "lint": "run-p lint:*", 9 | "build:doc": "redoc-cli bundle ./api.yml" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/sohosai/sos21-backend.git" 14 | }, 15 | "author": "coord_e", 16 | "license": "(MIT OR Apache-2.0)", 17 | "bugs": { 18 | "url": "https://github.com/sohosai/sos21-backend/issues" 19 | }, 20 | "homepage": "https://github.com/sohosai/sos21-backend#readme", 21 | "devDependencies": { 22 | "@stoplight/spectral": "^5.8.1", 23 | "npm-run-all": "^4.1.5", 24 | "prettier": "^2.2.1", 25 | "redoc-cli": "^0.10.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sos21-api-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use url::Url; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct Config { 7 | pub jwt_audience: String, 8 | pub jwt_issuer: String, 9 | pub jwt_keys_url: Url, 10 | pub postgres_uri: String, 11 | pub max_database_connections: u32, 12 | pub s3_access_key: String, 13 | pub s3_access_secret: String, 14 | pub s3_region: String, 15 | pub s3_endpoint: String, 16 | pub s3_object_bucket: String, 17 | pub administrator_email: String, 18 | pub project_creation_periods: HashMap, 19 | pub admin_report_slack_webhook: String, 20 | } 21 | -------------------------------------------------------------------------------- /sos21-api-server/src/filter/authentication/bearer.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use thiserror::Error; 4 | 5 | /// Decode `Bearer ` header value in [`FromStr`][from_str]. 6 | /// 7 | /// [from_str]: https://doc.rust-lang.org/std/str/trait.FromStr.html 8 | #[derive(Debug, Clone)] 9 | pub struct Bearer { 10 | pub token: String, 11 | } 12 | 13 | #[derive(Debug, Error, Clone)] 14 | #[error("invalid bearer authentication header value")] 15 | pub struct FromStrError { 16 | _priv: (), 17 | } 18 | 19 | impl FromStr for Bearer { 20 | type Err = FromStrError; 21 | fn from_str(s: &str) -> Result { 22 | let token = match s.trim().strip_prefix("Bearer") { 23 | Some(x) => x, 24 | None => return Err(FromStrError { _priv: () }), 25 | }; 26 | Ok(Bearer { 27 | token: token.trim().to_owned(), 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sos21-api-server/src/filter/authentication/claim.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | /// The decoded JWT claims returned from Firebase Authentication. 4 | /// https://firebase.google.com/docs/rules/rules-and-auth 5 | #[derive(Debug, Clone, Deserialize)] 6 | pub struct Claims { 7 | pub email: Option, 8 | pub email_verified: bool, 9 | pub phone_number: Option, 10 | pub name: Option, 11 | pub sub: String, 12 | // pub firebase.identities 13 | // pub firebase.sign_in_provider 14 | } 15 | -------------------------------------------------------------------------------- /sos21-api-server/src/filter/error/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{ser::Serializer, Serialize}; 2 | use warp::http::StatusCode; 3 | 4 | #[derive(Debug, Clone, Serialize)] 5 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 6 | pub enum AuthenticationErrorId { 7 | Unauthorized, 8 | InvalidToken, 9 | InvalidEmailAddress, 10 | UnverifiedEmailAddress, 11 | NotUniversityEmailAddress, 12 | NoEmail, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 17 | pub enum RequestErrorId { 18 | NotFound, 19 | UnsupportedMediaType, 20 | MissingHeader, 21 | MethodNotAllowed, 22 | InvalidHeader, 23 | InvalidQuery, 24 | InvalidBody, 25 | CorsForbidden, 26 | } 27 | 28 | #[derive(Debug, Clone, Serialize)] 29 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 30 | pub enum ErrorBody { 31 | Api { info: serde_json::Value }, 32 | Authentication { id: AuthenticationErrorId }, 33 | Request { id: RequestErrorId }, 34 | NotSignedUp, 35 | ServiceUnavailable, 36 | Internal, 37 | } 38 | 39 | #[derive(Debug, Clone, Serialize)] 40 | pub struct Error { 41 | pub error: ErrorBody, 42 | #[serde(serialize_with = "serialize_status_code")] 43 | pub status: StatusCode, 44 | } 45 | 46 | fn serialize_status_code(code: &StatusCode, serializer: S) -> Result 47 | where 48 | S: Serializer, 49 | { 50 | serializer.serialize_u16(code.as_u16()) 51 | } 52 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/file.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub use create::handler as create; 3 | pub mod get; 4 | pub use get::handler as get; 5 | pub mod share; 6 | pub use share::handler as share; 7 | pub mod get_info; 8 | pub use get_info::handler as get_info; 9 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/file/get.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Context; 2 | use crate::handler::model::file::{FileId, FileObject}; 3 | use crate::handler::{HandlerResponse, HandlerResult}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use sos21_domain::context::Login; 7 | use sos21_use_case::get_file_object; 8 | use warp::{http::StatusCode, reply}; 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | pub struct Request { 12 | pub file_id: FileId, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 17 | pub enum Error { 18 | FileNotFound, 19 | } 20 | 21 | impl HandlerResponse for Error { 22 | fn status_code(&self) -> StatusCode { 23 | match self { 24 | Error::FileNotFound => StatusCode::NOT_FOUND, 25 | } 26 | } 27 | } 28 | 29 | impl From for Error { 30 | fn from(err: get_file_object::Error) -> Error { 31 | match err { 32 | get_file_object::Error::NotFound => Error::FileNotFound, 33 | } 34 | } 35 | } 36 | 37 | #[macro_rules_attribute::macro_rules_attribute(raw_response_handler!)] 38 | pub async fn handler( 39 | ctx: Login, 40 | request: Request, 41 | ) -> HandlerResult { 42 | let file_object = get_file_object::run(&ctx, request.file_id.into_use_case()).await?; 43 | let file_object = FileObject::from_use_case(file_object); 44 | let reply = file_object.into_reply(); 45 | let reply = reply::with_status(reply, StatusCode::OK); 46 | Ok(reply) 47 | } 48 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/file/get_info.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Context; 2 | use crate::handler::model::file::{File, FileId}; 3 | use crate::handler::{HandlerResponse, HandlerResult}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use sos21_domain::context::Login; 7 | use sos21_use_case::get_file; 8 | use warp::http::StatusCode; 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | pub struct Request { 12 | pub file_id: FileId, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | pub struct Response { 17 | pub file: File, 18 | } 19 | 20 | impl HandlerResponse for Response { 21 | fn status_code(&self) -> StatusCode { 22 | StatusCode::OK 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 28 | pub enum Error { 29 | FileNotFound, 30 | } 31 | 32 | impl HandlerResponse for Error { 33 | fn status_code(&self) -> StatusCode { 34 | match self { 35 | Error::FileNotFound => StatusCode::NOT_FOUND, 36 | } 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(err: get_file::Error) -> Error { 42 | match err { 43 | get_file::Error::NotFound => Error::FileNotFound, 44 | } 45 | } 46 | } 47 | 48 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 49 | pub async fn handler(ctx: Login, request: Request) -> HandlerResult { 50 | let file = get_file::run(&ctx, request.file_id.into_use_case()).await?; 51 | let file = File::from_use_case(file); 52 | Ok(Response { file }) 53 | } 54 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/file_distribution.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub use create::handler as create; 3 | pub mod get; 4 | pub use get::handler as get; 5 | pub mod list; 6 | pub use list::handler as list; 7 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | pub mod revoke; 4 | pub use revoke::handler as revoke; 5 | pub mod get_file; 6 | pub use get_file::handler as get_file; 7 | pub mod get_file_info; 8 | pub use get_file_info::handler as get_file_info; 9 | pub mod get_public_file; 10 | pub use get_public_file::handler as get_public_file; 11 | pub mod get_public_file_info; 12 | pub use get_public_file_info::handler as get_public_file_info; 13 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/file_sharing/public.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub use get_file::handler as get_file; 3 | pub mod get_file_info; 4 | pub use get_file_info::handler as get_file_info; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/form.rs: -------------------------------------------------------------------------------- 1 | pub mod answer; 2 | 3 | pub mod create; 4 | pub use create::handler as create; 5 | pub mod update; 6 | pub use update::handler as update; 7 | pub mod get; 8 | pub use get::handler as get; 9 | pub mod list; 10 | pub use list::handler as list; 11 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/form/answer.rs: -------------------------------------------------------------------------------- 1 | pub mod list; 2 | pub use list::handler as list; 3 | pub mod export; 4 | pub use export::handler as export; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/form/list.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Context; 2 | use crate::handler::model::form::Form; 3 | use crate::handler::{HandlerResponse, HandlerResult}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use sos21_domain::context::Login; 7 | use sos21_use_case::list_all_forms; 8 | use warp::http::StatusCode; 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | pub struct Request {} 12 | 13 | #[derive(Debug, Clone, Serialize)] 14 | pub struct Response { 15 | pub forms: Vec
, 16 | } 17 | 18 | impl HandlerResponse for Response { 19 | fn status_code(&self) -> StatusCode { 20 | StatusCode::OK 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize)] 25 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 26 | pub enum Error { 27 | InsufficientPermissions, 28 | } 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match self { 33 | Error::InsufficientPermissions => StatusCode::FORBIDDEN, 34 | } 35 | } 36 | } 37 | 38 | impl From for Error { 39 | fn from(err: list_all_forms::Error) -> Error { 40 | match err { 41 | list_all_forms::Error::InsufficientPermissions => Error::InsufficientPermissions, 42 | } 43 | } 44 | } 45 | 46 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 47 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 48 | let forms = list_all_forms::run(&ctx).await?; 49 | let forms = forms.into_iter().map(Form::from_use_case).collect(); 50 | Ok(Response { forms }) 51 | } 52 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/form_answer.rs: -------------------------------------------------------------------------------- 1 | pub mod file_sharing; 2 | 3 | pub mod get; 4 | pub use get::handler as get; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/form_answer/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub use get_file::handler as get_file; 3 | pub mod get_file_info; 4 | pub use get_file_info::handler as get_file_info; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod file_sharing; 3 | pub mod pending_project; 4 | pub mod project; 5 | 6 | pub mod get; 7 | pub use get::handler as get; 8 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/file.rs: -------------------------------------------------------------------------------- 1 | pub mod list; 2 | pub use list::handler as list; 3 | pub mod check_usage; 4 | pub use check_usage::handler as check_usage; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/file/check_usage.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use crate::app::Context; 4 | use crate::handler::{HandlerResponse, HandlerResult}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use sos21_domain::context::Login; 8 | use sos21_use_case::get_user_file_usage; 9 | use warp::http::StatusCode; 10 | 11 | #[derive(Debug, Clone, Deserialize)] 12 | pub struct Request {} 13 | 14 | #[derive(Debug, Clone, Serialize)] 15 | pub struct Response { 16 | pub usage: u64, 17 | pub quota: Option, 18 | } 19 | 20 | impl HandlerResponse for Response { 21 | fn status_code(&self) -> StatusCode { 22 | StatusCode::OK 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 28 | pub enum Error {} 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match *self {} 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(x: Infallible) -> Error { 38 | match x {} 39 | } 40 | } 41 | 42 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 43 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 44 | let output = get_user_file_usage::run(&ctx).await?; 45 | Ok(Response { 46 | usage: output.usage, 47 | quota: output.quota, 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/file/list.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use crate::app::Context; 4 | use crate::handler::model::file::File; 5 | use crate::handler::{HandlerResponse, HandlerResult}; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use sos21_domain::context::Login; 9 | use sos21_use_case::list_user_files; 10 | use warp::http::StatusCode; 11 | 12 | #[derive(Debug, Clone, Deserialize)] 13 | pub struct Request {} 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | pub struct Response { 17 | pub files: Vec, 18 | } 19 | 20 | impl HandlerResponse for Response { 21 | fn status_code(&self) -> StatusCode { 22 | StatusCode::OK 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 28 | pub enum Error {} 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match *self {} 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(x: Infallible) -> Error { 38 | match x {} 39 | } 40 | } 41 | 42 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 43 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 44 | let files = list_user_files::run(&ctx).await?; 45 | let files = files.into_iter().map(File::from_use_case).collect(); 46 | Ok(Response { files }) 47 | } 48 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod list; 2 | pub use list::handler as list; 3 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/file_sharing/list.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use crate::app::Context; 4 | use crate::handler::model::file_sharing::FileSharing; 5 | use crate::handler::{HandlerResponse, HandlerResult}; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use sos21_domain::context::Login; 9 | use sos21_use_case::list_user_file_sharings; 10 | use warp::http::StatusCode; 11 | 12 | #[derive(Debug, Clone, Deserialize)] 13 | pub struct Request {} 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | pub struct Response { 17 | pub sharings: Vec, 18 | } 19 | 20 | impl HandlerResponse for Response { 21 | fn status_code(&self) -> StatusCode { 22 | StatusCode::OK 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 28 | pub enum Error {} 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match *self {} 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(x: Infallible) -> Error { 38 | match x {} 39 | } 40 | } 41 | 42 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 43 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 44 | let sharings = list_user_file_sharings::run(&ctx).await?; 45 | let sharings = sharings 46 | .into_iter() 47 | .map(FileSharing::from_use_case) 48 | .collect(); 49 | Ok(Response { sharings }) 50 | } 51 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/get.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use crate::app::Context; 4 | use crate::handler::model::user::User; 5 | use crate::handler::{HandlerResponse, HandlerResult}; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use sos21_domain::context::Login; 9 | use sos21_use_case::get_login_user; 10 | use warp::http::StatusCode; 11 | 12 | #[derive(Debug, Clone, Deserialize)] 13 | pub struct Request {} 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | pub struct Response { 17 | pub user: User, 18 | } 19 | 20 | impl HandlerResponse for Response { 21 | fn status_code(&self) -> StatusCode { 22 | StatusCode::OK 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 28 | pub enum Error {} 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match *self {} 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(x: Infallible) -> Error { 38 | match x {} 39 | } 40 | } 41 | 42 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 43 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 44 | let user = get_login_user::run(&ctx).await?; 45 | let user = User::from_use_case(user); 46 | Ok(Response { user }) 47 | } 48 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/pending_project.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/project.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/me/project/get.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Context; 2 | use crate::handler::model::project::Project; 3 | use crate::handler::{HandlerResponse, HandlerResult}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use sos21_domain::context::Login; 7 | use sos21_use_case::get_user_project; 8 | use warp::http::StatusCode; 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | pub struct Request {} 12 | 13 | #[derive(Debug, Clone, Serialize)] 14 | pub struct Response { 15 | pub project: Project, 16 | } 17 | 18 | impl HandlerResponse for Response { 19 | fn status_code(&self) -> StatusCode { 20 | StatusCode::OK 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize)] 25 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 26 | pub enum Error { 27 | ProjectNotFound, 28 | } 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match self { 33 | Error::ProjectNotFound => StatusCode::NOT_FOUND, 34 | } 35 | } 36 | } 37 | 38 | impl From for Error { 39 | fn from(err: get_user_project::Error) -> Error { 40 | match err { 41 | get_user_project::Error::NotFound => Error::ProjectNotFound, 42 | } 43 | } 44 | } 45 | 46 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 47 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 48 | let project = get_user_project::run(&ctx).await?; 49 | let project = Project::from_use_case(project); 50 | Ok(Response { project }) 51 | } 52 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/meta.rs: -------------------------------------------------------------------------------- 1 | pub mod get_build_info; 2 | pub mod health; 3 | pub use get_build_info::handler as get_build_info; 4 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/meta/health.rs: -------------------------------------------------------------------------------- 1 | pub mod check; 2 | pub use check::handler as check; 3 | pub mod check_liveness; 4 | pub use check_liveness::handler as check_liveness; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/meta/health/check_liveness.rs: -------------------------------------------------------------------------------- 1 | use crate::handler::{HandlerResponse, HandlerResult}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use warp::http::StatusCode; 5 | 6 | #[derive(Debug, Clone, Deserialize)] 7 | pub struct Request {} 8 | 9 | #[derive(Debug, Clone, Serialize)] 10 | pub struct Response; 11 | 12 | impl HandlerResponse for Response { 13 | fn status_code(&self) -> StatusCode { 14 | StatusCode::OK 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone, Serialize)] 19 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 20 | pub enum Error {} 21 | 22 | impl HandlerResponse for Error { 23 | fn status_code(&self) -> StatusCode { 24 | match *self {} 25 | } 26 | } 27 | 28 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 29 | pub async fn handler(_request: Request) -> HandlerResult { 30 | Ok(Response) 31 | } 32 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/model.rs: -------------------------------------------------------------------------------- 1 | //! API model types. 2 | //! 3 | //! API's model types are implemented independently from `sos21_use_case::model`. 4 | //! We never use data transfer object from `sos21-use-case` directly as API's model types, 5 | //! because it's horribly not good that changes in `sos21-use-case` DTO affects 6 | //! the public interface, or specification, of the API server. 7 | //! In the other words, we want to keep the public interface of the API server self-contained in `sos21-api-server`. 8 | 9 | mod serde; 10 | 11 | pub mod date_time; 12 | pub mod distributed_file; 13 | pub mod file; 14 | pub mod file_distribution; 15 | pub mod file_sharing; 16 | pub mod form; 17 | pub mod form_answer; 18 | pub mod pending_project; 19 | pub mod project; 20 | pub mod project_query; 21 | pub mod registration_form; 22 | pub mod registration_form_answer; 23 | pub mod user; 24 | pub mod user_invitation; 25 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/model/date_time.rs: -------------------------------------------------------------------------------- 1 | use chrono::{serde::ts_milliseconds, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 5 | #[serde(transparent)] 6 | pub struct DateTime(#[serde(with = "ts_milliseconds")] pub chrono::DateTime); 7 | 8 | impl DateTime { 9 | pub fn from_use_case(utc: chrono::DateTime) -> Self { 10 | DateTime(utc) 11 | } 12 | 13 | pub fn into_use_case(self) -> chrono::DateTime { 14 | self.0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/model/distributed_file.rs: -------------------------------------------------------------------------------- 1 | use crate::handler::model::date_time::DateTime; 2 | use crate::handler::model::file_distribution::FileDistributionId; 3 | use crate::handler::model::file_sharing::FileSharingId; 4 | use crate::handler::model::project::ProjectId; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use sos21_use_case::model::file_distribution as use_case; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | pub struct DistributedFile { 11 | pub distribution_id: FileDistributionId, 12 | pub distributed_at: DateTime, 13 | pub name: String, 14 | pub description: String, 15 | pub project_id: ProjectId, 16 | pub sharing_id: FileSharingId, 17 | } 18 | 19 | impl DistributedFile { 20 | pub fn from_use_case(distributed_file: use_case::FileDistributionDistributedFile) -> Self { 21 | DistributedFile { 22 | distribution_id: FileDistributionId::from_use_case(distributed_file.distribution_id), 23 | distributed_at: DateTime::from_use_case(distributed_file.distributed_at), 24 | name: distributed_file.name, 25 | description: distributed_file.description, 26 | project_id: ProjectId::from_use_case(distributed_file.project_id), 27 | sharing_id: FileSharingId::from_use_case(distributed_file.sharing_id), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/model/form/item/checkbox.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sos21_use_case::model::form::item as use_case; 3 | use uuid::Uuid; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(transparent)] 7 | pub struct CheckboxId(pub Uuid); 8 | 9 | impl CheckboxId { 10 | pub fn from_use_case(id: use_case::CheckboxId) -> Self { 11 | CheckboxId(id.0) 12 | } 13 | 14 | pub fn into_use_case(self) -> use_case::CheckboxId { 15 | use_case::CheckboxId(self.0) 16 | } 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct Checkbox { 21 | pub id: CheckboxId, 22 | pub label: String, 23 | } 24 | 25 | impl Checkbox { 26 | pub fn from_use_case(checkbox: use_case::Checkbox) -> Self { 27 | Checkbox { 28 | id: CheckboxId::from_use_case(checkbox.id), 29 | label: checkbox.label, 30 | } 31 | } 32 | 33 | pub fn into_use_case(self) -> use_case::Checkbox { 34 | use_case::Checkbox { 35 | id: self.id.into_use_case(), 36 | label: self.label, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/model/form/item/radio.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sos21_use_case::model::form::item as use_case; 3 | use uuid::Uuid; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(transparent)] 7 | pub struct RadioId(pub Uuid); 8 | 9 | impl RadioId { 10 | pub fn from_use_case(id: use_case::RadioId) -> Self { 11 | RadioId(id.0) 12 | } 13 | 14 | pub fn into_use_case(self) -> use_case::RadioId { 15 | use_case::RadioId(self.0) 16 | } 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct Radio { 21 | pub id: RadioId, 22 | pub label: String, 23 | } 24 | 25 | impl Radio { 26 | pub fn from_use_case(button: use_case::Radio) -> Self { 27 | Radio { 28 | id: RadioId::from_use_case(button.id), 29 | label: button.label, 30 | } 31 | } 32 | 33 | pub fn into_use_case(self) -> use_case::Radio { 34 | use_case::Radio { 35 | id: self.id.into_use_case(), 36 | label: self.label, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/pending_project.rs: -------------------------------------------------------------------------------- 1 | pub mod registration_form; 2 | 3 | pub mod get; 4 | pub use get::handler as get; 5 | pub mod update; 6 | pub use update::handler as update; 7 | pub mod update_any; 8 | pub use update_any::handler as update_any; 9 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/pending_project/registration_form.rs: -------------------------------------------------------------------------------- 1 | pub mod answer; 2 | pub use answer::handler as answer; 3 | pub mod get; 4 | pub use get::handler as get; 5 | pub mod list; 6 | pub use list::handler as list; 7 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project.rs: -------------------------------------------------------------------------------- 1 | pub mod prepare; 2 | pub use prepare::handler as prepare; 3 | pub mod create; 4 | pub use create::handler as create; 5 | pub mod update; 6 | pub use update::handler as update; 7 | pub mod update_any; 8 | pub use update_any::handler as update_any; 9 | pub mod get; 10 | pub use get::handler as get; 11 | pub mod list; 12 | pub use list::handler as list; 13 | pub mod export; 14 | pub use export::handler as export; 15 | 16 | pub mod file_distribution; 17 | pub mod file_sharing; 18 | pub mod form; 19 | pub mod registration_form; 20 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/file_distribution.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | pub mod list; 4 | pub use list::handler as list; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub use get_file::handler as get_file; 3 | pub mod get_file_info; 4 | pub use get_file_info::handler as get_file_info; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/form.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | pub mod answer; 4 | pub use answer::handler as answer; 5 | pub mod list; 6 | pub use list::handler as list; 7 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/form/answer/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub use get_file::handler as get_file; 3 | pub mod get_file_info; 4 | pub use get_file_info::handler as get_file_info; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/registration_form.rs: -------------------------------------------------------------------------------- 1 | pub mod answer; 2 | 3 | pub mod get; 4 | pub use get::handler as get; 5 | pub mod list; 6 | pub use list::handler as list; 7 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/registration_form/answer.rs: -------------------------------------------------------------------------------- 1 | pub mod file_sharing; 2 | 3 | pub mod get; 4 | pub use get::handler as get; 5 | pub mod update; 6 | pub use update::handler as update; 7 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project/registration_form/answer/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub use get_file::handler as get_file; 3 | pub mod get_file_info; 4 | pub use get_file_info::handler as get_file_info; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/project_creation_availability.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/registration_form.rs: -------------------------------------------------------------------------------- 1 | pub mod answer; 2 | 3 | pub mod create; 4 | pub use create::handler as create; 5 | pub mod get; 6 | pub use get::handler as get; 7 | pub mod list; 8 | pub use list::handler as list; 9 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/registration_form/answer.rs: -------------------------------------------------------------------------------- 1 | pub mod list; 2 | pub use list::handler as list; 3 | pub mod export; 4 | pub use export::handler as export; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/registration_form_answer.rs: -------------------------------------------------------------------------------- 1 | pub mod file_sharing; 2 | 3 | pub mod get; 4 | pub use get::handler as get; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/registration_form_answer/file_sharing.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub use get_file::handler as get_file; 3 | pub mod get_file_info; 4 | pub use get_file_info::handler as get_file_info; 5 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/user.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | pub mod list; 4 | pub use list::handler as list; 5 | pub mod update; 6 | pub use update::handler as update; 7 | pub mod export; 8 | pub use export::handler as export; 9 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/user/get.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Context; 2 | use crate::handler::model::user::{User, UserId}; 3 | use crate::handler::{HandlerResponse, HandlerResult}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use sos21_domain::context::Login; 7 | use sos21_use_case::get_user; 8 | use warp::http::StatusCode; 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | pub struct Request { 12 | pub user_id: UserId, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | pub struct Response { 17 | pub user: User, 18 | } 19 | 20 | impl HandlerResponse for Response { 21 | fn status_code(&self) -> StatusCode { 22 | StatusCode::OK 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 28 | pub enum Error { 29 | UserNotFound, 30 | } 31 | 32 | impl HandlerResponse for Error { 33 | fn status_code(&self) -> StatusCode { 34 | match self { 35 | Error::UserNotFound => StatusCode::NOT_FOUND, 36 | } 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(err: get_user::Error) -> Error { 42 | match err { 43 | get_user::Error::NotFound => Error::UserNotFound, 44 | } 45 | } 46 | } 47 | 48 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 49 | pub async fn handler(ctx: Login, request: Request) -> HandlerResult { 50 | let user = get_user::run(&ctx, request.user_id.into_use_case()).await?; 51 | Ok(Response { 52 | user: User::from_use_case(user), 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/user/list.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Context; 2 | use crate::handler::model::user::User; 3 | use crate::handler::{HandlerResponse, HandlerResult}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use sos21_domain::context::Login; 7 | use sos21_use_case::list_users; 8 | use warp::http::StatusCode; 9 | 10 | #[derive(Debug, Clone, Deserialize)] 11 | pub struct Request {} 12 | 13 | #[derive(Debug, Clone, Serialize)] 14 | pub struct Response { 15 | pub users: Vec, 16 | } 17 | 18 | impl HandlerResponse for Response { 19 | fn status_code(&self) -> StatusCode { 20 | StatusCode::OK 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize)] 25 | #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] 26 | pub enum Error { 27 | InsufficientPermissions, 28 | } 29 | 30 | impl HandlerResponse for Error { 31 | fn status_code(&self) -> StatusCode { 32 | match self { 33 | Error::InsufficientPermissions => StatusCode::FORBIDDEN, 34 | } 35 | } 36 | } 37 | 38 | impl From for Error { 39 | fn from(err: list_users::Error) -> Error { 40 | match err { 41 | list_users::Error::InsufficientPermissions => Error::InsufficientPermissions, 42 | } 43 | } 44 | } 45 | 46 | #[macro_rules_attribute::macro_rules_attribute(handler!)] 47 | pub async fn handler(ctx: Login, _request: Request) -> HandlerResult { 48 | let users = list_users::run(&ctx).await?; 49 | let users = users.into_iter().map(User::from_use_case).collect(); 50 | Ok(Response { users }) 51 | } 52 | -------------------------------------------------------------------------------- /sos21-api-server/src/handler/user_invitation.rs: -------------------------------------------------------------------------------- 1 | pub mod get; 2 | pub use get::handler as get; 3 | pub mod list; 4 | pub use list::handler as list; 5 | pub mod delete; 6 | pub use delete::handler as delete; 7 | -------------------------------------------------------------------------------- /sos21-api-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod config; 3 | mod server; 4 | 5 | pub mod filter; 6 | pub mod handler; 7 | 8 | pub use config::Config; 9 | pub use server::Server; 10 | -------------------------------------------------------------------------------- /sos21-database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-database" 3 | version = "0.7.1" 4 | authors = ["coord_e ", "azarashi2931 ", "yuseiito ", "momeemt "] 5 | edition = "2018" 6 | readme = "README.md" 7 | documentation = "https://sohosai.github.io/sos21-backend/sos21_database/" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | chrono = "0.4" 13 | uuid = "0.8" 14 | futures = "0.3" 15 | bitflags = "1" 16 | serde_json = "1" 17 | 18 | [dependencies.sqlx] 19 | version = "0.5" 20 | features = [ 21 | "bit-vec", 22 | "chrono", 23 | "json", 24 | "macros", 25 | "offline", 26 | "postgres", 27 | "runtime-tokio-rustls", 28 | "uuid", 29 | ] 30 | 31 | [build-dependencies] 32 | syn = "1" -------------------------------------------------------------------------------- /sos21-database/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-database` 2 | 3 | [![docs (develop)](https://img.shields.io/badge/docs-develop-blue)](https://sohosai.github.io/sos21-backend/develop/sos21_database/) 4 | [![docs](https://img.shields.io/github/v/release/sohosai/sos21-backend?label=docs&color=blue)](https://sohosai.github.io/sos21-backend/sos21_database/) 5 | 6 | データベースとのインターフェースを提供します。 7 | 8 | SQL を叩く関数群とデータモデルを実装しています。 9 | `command` モジュールはデータを変更する操作、`query` モジュールはデータを変更しない操作を含んでいます。 10 | 11 | ## マイグレーション 12 | 13 | `nix-shell` 内で次のコマンドを実行し、マイグレーションを適用します。 14 | 15 | ```shell 16 | $ cd sos21-database 17 | $ sqlx migrate run 18 | ``` 19 | 20 | ## SQL のコンパイル時検証について 21 | 22 | [`sqlx`](https://github.com/launchbadge/sqlx) クレートを用いて、 23 | 記述した SQL がある程度妥当なものであることをコンパイル時に検証しています。 24 | 25 | `DATABASE_URL` 環境変数が設定されている場合は実際にデータベースに問い合わせることで検証を行い、 26 | そうでない場合は `sqlx-data.json` に保存されている情報に基づいて検証を行います。 27 | 28 | そのため、CI でビルドするためには `sqlx-data.json` を最新の状態に保つ必要があります。 29 | `nix-shell` 内で次のコマンドを実行することで `sqlx-data.json` を更新することができます。 30 | 31 | ```shell 32 | $ cd sos21-database 33 | $ cargo sqlx prepare 34 | ``` 35 | 36 | 詳しくは [Enable building in "offline" mode with `query!()`](https://github.com/launchbadge/sqlx/blob/master/sqlx-cli/README.md#enable-building-in-offline-mode-with-query) を参照してください。 37 | -------------------------------------------------------------------------------- /sos21-database/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-database 8 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210302103321_add_project_index.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN index smallint UNIQUE CHECK (index BETWEEN 0 AND 999); 2 | UPDATE projects SET index = numbers.index 3 | FROM (SELECT id, (row_number() over () - 1) as index FROM projects) 4 | AS numbers 5 | WHERE projects.id = numbers.id; 6 | ALTER TABLE projects ALTER COLUMN index SET NOT NULL; 7 | 8 | CREATE SEQUENCE count_projects AS smallint MINVALUE 0 MAXVALUE 999; 9 | SELECT setval('count_projects', (SELECT count(*) FROM projects), false); 10 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210302170442_add_cooking_food_outdoor.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE project_category ADD VALUE 'cooking'; 2 | ALTER TYPE project_category ADD VALUE 'food'; 3 | 4 | ALTER TABLE forms ALTER COLUMN unspecified_query 5 | SET DATA TYPE bit(16) 6 | USING unspecified_query || B'00000000'; 7 | ALTER TABLE forms ALTER COLUMN general_query 8 | SET DATA TYPE bit(16) 9 | USING general_query || B'00000000'; 10 | ALTER TABLE forms ALTER COLUMN stage_query 11 | SET DATA TYPE bit(16) 12 | USING stage_query || B'00000000'; 13 | 14 | ALTER TABLE forms ADD COLUMN food_query bit(16); 15 | ALTER TABLE forms ADD COLUMN cooking_query bit(16); 16 | UPDATE forms SET food_query = B'0000000000000000', cooking_query = B'0000000000000000'; 17 | ALTER TABLE forms ALTER COLUMN food_query SET NOT NULL; 18 | ALTER TABLE forms ALTER COLUMN cooking_query SET NOT NULL; 19 | 20 | ALTER TABLE forms ADD COLUMN needs_sync boolean; 21 | UPDATE forms SET needs_sync = true; 22 | ALTER TABLE forms ALTER COLUMN needs_sync SET NOT NULL; 23 | 24 | DROP FUNCTION form_query_match(bit(8), integer); 25 | CREATE FUNCTION form_query_match(bit(16), integer) 26 | RETURNS boolean 27 | IMMUTABLE LEAKPROOF 28 | LANGUAGE SQL AS $$ 29 | SELECT $1 & (B'10000000000000000' >> $2::integer) <> B'0000000000000000'; 30 | $$; 31 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210302200024_delete_display_id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects DROP COLUMN display_id; 2 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210309032212_structured_project_query.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | ASSERT 3 | (SELECT count(*) = 0 FROM forms), 4 | 'Manual migration is required some when forms are already stored'; 5 | END $$; 6 | 7 | ALTER TABLE forms DROP COLUMN condition; 8 | ALTER TABLE forms DROP COLUMN unspecified_query; 9 | ALTER TABLE forms DROP COLUMN general_query; 10 | ALTER TABLE forms DROP COLUMN stage_query; 11 | ALTER TABLE forms DROP COLUMN cooking_query; 12 | ALTER TABLE forms DROP COLUMN food_query; 13 | ALTER TABLE forms DROP COLUMN needs_sync; 14 | 15 | DROP FUNCTION form_query_match(bit(16), integer); 16 | 17 | CREATE TABLE form_project_query_conjunctions ( 18 | form_id uuid NOT NULL REFERENCES forms ON DELETE RESTRICT, 19 | category project_category, 20 | attributes integer NOT NULL 21 | ); 22 | 23 | CREATE INDEX form_project_query_conjunctions_form_id_idx 24 | ON form_project_query_conjunctions ( form_id ); 25 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210311070210_json_form_data.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | ASSERT 3 | (SELECT count(*) = 0 FROM forms), 4 | 'Manual migration is required some when forms are already stored'; 5 | END $$; 6 | 7 | ALTER TABLE forms DROP COLUMN items; 8 | ALTER TABLE forms ADD COLUMN items jsonb NOT NULL; 9 | 10 | ALTER TABLE form_answers DROP COLUMN items; 11 | ALTER TABLE form_answers ADD COLUMN items jsonb NOT NULL; 12 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210311073742_delete_unused_count_project_sequence.sql: -------------------------------------------------------------------------------- 1 | DROP SEQUENCE count_projects; 2 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210311165742_add_files.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE files ( 2 | id uuid PRIMARY KEY, 3 | created_at timestamptz NOT NULL, 4 | author_id varchar(64) NOT NULL REFERENCES users ON DELETE RESTRICT, 5 | object_id uuid UNIQUE NOT NULL, 6 | blake3_digest bytea NOT NULL, 7 | name varchar(255), 8 | type_ varchar(255) NOT NULL, 9 | size bigint NOT NULL 10 | ); 11 | 12 | CREATE INDEX files_author_id_idx ON files ( author_id ); 13 | CREATE INDEX files_blake3_digest_idx ON files ( blake3_digest ); 14 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210330101458_pending_project_subowner.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pending_projects ( 2 | id uuid PRIMARY KEY, 3 | created_at timestamptz NOT NULL, 4 | author_id varchar(64) NOT NULL REFERENCES users ON DELETE RESTRICT, 5 | name varchar(128) NOT NULL, 6 | kana_name varchar(512) NOT NULL, 7 | group_name varchar(128) NOT NULL, 8 | kana_group_name varchar(512) NOT NULL, 9 | description varchar(4096) NOT NULL, 10 | category project_category NOT NULL, 11 | attributes integer NOT NULL 12 | ); 13 | 14 | INSERT INTO pending_projects 15 | SELECT 16 | id, 17 | now() AS created_at, 18 | owner_id AS author_id, 19 | name, 20 | kana_name, 21 | group_name, 22 | kana_group_name, 23 | description, 24 | category, 25 | attributes 26 | FROM projects; 27 | 28 | DELETE FROM projects; 29 | 30 | ALTER TABLE projects ADD COLUMN subowner_id varchar(64) NOT NULL REFERENCES users ON DELETE RESTRICT; 31 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210412123257_user_category.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE user_category AS ENUM ('undergraduate_student', 'graduate_student', 'academic_staff'); 2 | 3 | ALTER TABLE users ADD COLUMN category user_category; 4 | UPDATE users SET category = 'undergraduate_student'; 5 | ALTER TABLE users ALTER COLUMN category SET NOT NULL; 6 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210429135555_user_invitation.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD CONSTRAINT user_email UNIQUE ( email ); 2 | 3 | CREATE TYPE user_invitation_role AS ENUM ('administrator', 'committee_operator', 'committee'); 4 | 5 | CREATE TABLE user_invitations ( 6 | id uuid PRIMARY KEY, 7 | created_at timestamptz NOT NULL, 8 | author_id varchar(64) NOT NULL REFERENCES users ON DELETE RESTRICT, 9 | email varchar(128) UNIQUE NOT NULL, 10 | role user_invitation_role NOT NULL 11 | ); 12 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210520143144_affiliation_user_category.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ALTER COLUMN affiliation DROP NOT NULL, 3 | ADD CONSTRAINT users_affiliation_category CHECK ( 4 | (category = 'undergraduate_student') = (affiliation IS NOT NULL) 5 | ); 6 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210615083430_add_project_query_file_sharing.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE file_sharing_scope ADD VALUE 'project_query'; 2 | COMMIT; -- we need to commit current transaction before we use 'project_query' enum value 3 | 4 | ALTER TABLE file_sharings 5 | ADD COLUMN project_query jsonb, 6 | ADD CONSTRAINT file_sharings_scope_project_query CHECK ( 7 | (scope = 'project_query') = (project_query IS NOT NULL) 8 | ); 9 | -------------------------------------------------------------------------------- /sos21-database/migrations/20210715163345_last_update.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN updated_at timestamptz; 2 | UPDATE projects SET updated_at = created_at; 3 | ALTER TABLE projects ALTER COLUMN updated_at SET NOT NULL; 4 | 5 | ALTER TABLE pending_projects ADD COLUMN updated_at timestamptz; 6 | UPDATE pending_projects SET updated_at = created_at; 7 | ALTER TABLE pending_projects ALTER COLUMN updated_at SET NOT NULL; 8 | 9 | ALTER TABLE registration_form_answers ADD COLUMN updated_at timestamptz; 10 | UPDATE registration_form_answers SET updated_at = created_at; 11 | ALTER TABLE registration_form_answers ALTER COLUMN updated_at SET NOT NULL; 12 | -------------------------------------------------------------------------------- /sos21-database/migrations/20220412141856_delete_affiliation.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TABLE users 3 | DROP CONSTRAINT users_affiliation_category, 4 | DROP COLUMN "affiliation"; 5 | -------------------------------------------------------------------------------- /sos21-database/migrations/20220414211708_add-project-category-enum.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TYPE project_category ADD VALUE 'general_online'; 3 | ALTER TYPE project_category ADD VALUE 'general_physical'; 4 | ALTER TYPE project_category ADD VALUE 'stage_online'; 5 | ALTER TYPE project_category ADD VALUE 'stage_physical'; 6 | ALTER TYPE project_category ADD VALUE 'cooking_physical'; 7 | ALTER TYPE project_category ADD VALUE 'food_physical'; 8 | -------------------------------------------------------------------------------- /sos21-database/migrations/20220425063347_slack-notification.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TABLE forms ADD COLUMN answer_notification_webhook varchar(100); 3 | -------------------------------------------------------------------------------- /sos21-database/migrations/20220622022603_exceptional_complete_deadline.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pending_projects ADD COLUMN exceptional_complete_deadline timestamptz CHECK (current_timestamp < exceptional_complete_deadline); 2 | -------------------------------------------------------------------------------- /sos21-database/migrations/20230423025208_redefine_project_category.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE project_category RENAME TO project_category_old; 2 | CREATE TYPE project_category AS ENUM('general', 'cooking_requiring_preparation_area', 'cooking', 'food', 'stage'); 3 | ALTER TABLE projects ALTER COLUMN category TYPE project_category USING category::text::project_category; 4 | ALTER TABLE pending_projects ALTER COLUMN category TYPE project_category USING category::text::project_category; 5 | ALTER TABLE form_project_query_conjunctions ALTER category TYPE project_category USING category::text::project_category; 6 | ALTER TABLE registration_form_project_query_conjunctions ALTER category TYPE project_category USING category::text::project_category; 7 | DROP TYPE project_category_old; 8 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_file_distribution_files.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_file_distribution_files<'a, E>(conn: E, distribution_id: Uuid) -> Result<()> 5 | where 6 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 7 | { 8 | sqlx::query!( 9 | r#" 10 | DELETE FROM file_distribution_files 11 | WHERE distribution_id = $1 12 | "#, 13 | distribution_id, 14 | ) 15 | .execute(conn) 16 | .await 17 | .context("Failed to delete from file distribution files")?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_form_condition_excludes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_form_condition_excludes<'a, E>( 5 | conn: E, 6 | form_id: Uuid, 7 | project_ids: Vec, 8 | ) -> Result<()> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | sqlx::query!( 13 | r#" 14 | DELETE FROM form_condition_excludes 15 | WHERE project_id = ANY ($2) AND form_id = $1 16 | "#, 17 | form_id, 18 | &project_ids, 19 | ) 20 | .execute(conn) 21 | .await 22 | .context("Failed to delete form condition excludes")?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_form_condition_includes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_form_condition_includes<'a, E>( 5 | conn: E, 6 | form_id: Uuid, 7 | project_ids: Vec, 8 | ) -> Result<()> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | sqlx::query!( 13 | r#" 14 | DELETE FROM form_condition_includes 15 | WHERE project_id = ANY ($2) AND form_id = $1 16 | "#, 17 | form_id, 18 | &project_ids, 19 | ) 20 | .execute(conn) 21 | .await 22 | .context("Failed to delete form condition includes")?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_form_project_query_conjunctions.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_form_project_query_conjunctions<'a, E>(conn: E, form_id: Uuid) -> Result<()> 5 | where 6 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 7 | { 8 | sqlx::query!( 9 | r#" 10 | DELETE FROM form_project_query_conjunctions 11 | WHERE form_id = $1 12 | "#, 13 | form_id, 14 | ) 15 | .execute(conn) 16 | .await 17 | .context("Failed to delete from form project query conjunctions")?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_pending_project.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_pending_project<'a, E>(conn: E, id: Uuid) -> Result<()> 5 | where 6 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 7 | { 8 | sqlx::query!( 9 | r#" 10 | DELETE FROM pending_projects 11 | WHERE id = $1 12 | "#, 13 | id, 14 | ) 15 | .execute(conn) 16 | .await 17 | .context("Failed to delete from pending projects")?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_registration_form_project_query_conjunctions.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_registration_form_project_query_conjunctions<'a, E>( 5 | conn: E, 6 | registration_form_id: Uuid, 7 | ) -> Result<()> 8 | where 9 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 10 | { 11 | sqlx::query!( 12 | r#" 13 | DELETE FROM registration_form_project_query_conjunctions 14 | WHERE registration_form_id = $1 15 | "#, 16 | registration_form_id, 17 | ) 18 | .execute(conn) 19 | .await 20 | .context("Failed to delete from registration form project query conjunctions")?; 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/command/delete_user_invitation.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn delete_user_invitation<'a, E>(conn: E, id: Uuid) -> Result<()> 5 | where 6 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 7 | { 8 | sqlx::query!("DELETE FROM user_invitations where id = $1", id) 9 | .execute(conn) 10 | .await 11 | .context("Failed to delete from user invitations")?; 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_file.rs: -------------------------------------------------------------------------------- 1 | use crate::model::file::File; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_file<'a, E>(conn: E, file: File) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let File { 10 | id, 11 | created_at, 12 | author_id, 13 | object_id, 14 | blake3_digest, 15 | name, 16 | type_, 17 | size, 18 | } = file; 19 | 20 | sqlx::query!( 21 | r#" 22 | INSERT INTO files ( 23 | id, 24 | created_at, 25 | author_id, 26 | object_id, 27 | blake3_digest, 28 | name, 29 | type_, 30 | size 31 | ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8 ) 32 | "#, 33 | id, 34 | created_at, 35 | author_id, 36 | object_id, 37 | blake3_digest, 38 | name, 39 | type_, 40 | size 41 | ) 42 | .execute(conn) 43 | .await 44 | .context("Failed to insert to files")?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_file_distribution.rs: -------------------------------------------------------------------------------- 1 | use crate::model::file_distribution::FileDistribution; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_file_distribution<'a, E>(conn: E, distribution: FileDistribution) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let FileDistribution { 10 | id, 11 | created_at, 12 | author_id, 13 | name, 14 | description, 15 | } = distribution; 16 | 17 | sqlx::query!( 18 | r#" 19 | INSERT INTO file_distributions ( 20 | id, 21 | created_at, 22 | author_id, 23 | name, 24 | description 25 | ) VALUES ( $1, $2, $3, $4, $5 ) 26 | "#, 27 | id, 28 | created_at, 29 | author_id, 30 | name, 31 | description 32 | ) 33 | .execute(conn) 34 | .await 35 | .context("Failed to insert to file distributions")?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_file_distribution_files.rs: -------------------------------------------------------------------------------- 1 | use crate::model::file_distribution::FileDistributionFile; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn insert_file_distribution_files<'a, E>( 7 | conn: E, 8 | distribution_id: Uuid, 9 | files: Vec, 10 | ) -> Result<()> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 13 | { 14 | let mut project_ids = Vec::new(); 15 | let mut sharing_ids = Vec::new(); 16 | 17 | for file in files { 18 | project_ids.push(file.project_id); 19 | sharing_ids.push(file.sharing_id); 20 | } 21 | 22 | sqlx::query!( 23 | r#" 24 | INSERT INTO file_distribution_files ( 25 | distribution_id, 26 | project_id, 27 | sharing_id 28 | ) 29 | SELECT 30 | $1 AS distribution_id, 31 | file.project_id, 32 | file.sharing_id 33 | FROM unnest( 34 | $2::uuid[], 35 | $3::uuid[] 36 | ) AS file( 37 | project_id, 38 | sharing_id 39 | ) 40 | "#, 41 | distribution_id, 42 | &project_ids, 43 | &sharing_ids 44 | ) 45 | .execute(conn) 46 | .await 47 | .context("Failed to insert to file distribution files")?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_form.rs: -------------------------------------------------------------------------------- 1 | use crate::model::form::Form; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_form<'a, E>(conn: E, form: Form) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let Form { 10 | id, 11 | created_at, 12 | author_id, 13 | name, 14 | description, 15 | starts_at, 16 | ends_at, 17 | items, 18 | answer_notification_webhook, 19 | } = form; 20 | 21 | sqlx::query!( 22 | r#" 23 | INSERT INTO forms ( 24 | id, 25 | created_at, 26 | author_id, 27 | name, 28 | description, 29 | starts_at, 30 | ends_at, 31 | items, 32 | answer_notification_webhook 33 | ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8 ,$9) 34 | "#, 35 | id, 36 | created_at, 37 | author_id, 38 | name, 39 | description, 40 | starts_at, 41 | ends_at, 42 | items, 43 | answer_notification_webhook 44 | ) 45 | .execute(conn) 46 | .await 47 | .context("Failed to insert to forms")?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_form_answer.rs: -------------------------------------------------------------------------------- 1 | use crate::model::form_answer::FormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_form_answer<'a, E>(conn: E, answer: FormAnswer) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let FormAnswer { 10 | id, 11 | created_at, 12 | author_id, 13 | form_id, 14 | project_id, 15 | items, 16 | } = answer; 17 | 18 | sqlx::query!( 19 | r#" 20 | INSERT INTO form_answers ( 21 | id, 22 | created_at, 23 | author_id, 24 | form_id, 25 | project_id, 26 | items 27 | ) VALUES ( $1, $2, $3, $4, $5, $6 ) 28 | "#, 29 | id, 30 | created_at, 31 | author_id, 32 | form_id, 33 | project_id, 34 | items, 35 | ) 36 | .execute(conn) 37 | .await 38 | .context("Failed to insert to form answers")?; 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_form_condition_excludes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn insert_form_condition_excludes<'a, E>( 5 | conn: E, 6 | form_id: Uuid, 7 | exclude_ids: Vec, 8 | ) -> Result<()> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | sqlx::query!( 13 | r#" 14 | INSERT INTO form_condition_excludes ( 15 | project_id, 16 | form_id 17 | ) 18 | SELECT 19 | exclude_ids.id AS project_id, 20 | $1 AS form_id 21 | FROM unnest($2::uuid[]) AS exclude_ids( id ) 22 | "#, 23 | form_id, 24 | &exclude_ids 25 | ) 26 | .execute(conn) 27 | .await 28 | .context("Failed to insert to form condition excludes")?; 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_form_condition_includes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | pub async fn insert_form_condition_includes<'a, E>( 5 | conn: E, 6 | form_id: Uuid, 7 | include_ids: Vec, 8 | ) -> Result<()> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | sqlx::query!( 13 | r#" 14 | INSERT INTO form_condition_includes ( 15 | project_id, 16 | form_id 17 | ) 18 | SELECT 19 | include_ids.id AS project_id, 20 | $1 AS form_id 21 | FROM unnest($2::uuid[]) AS include_ids( id ) 22 | "#, 23 | form_id, 24 | &include_ids 25 | ) 26 | .execute(conn) 27 | .await 28 | .context("Failed to insert to form condition includes")?; 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_pending_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::pending_project::PendingProject; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_pending_project<'a, E>(conn: E, pending_project: PendingProject) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let PendingProject { 10 | id, 11 | created_at, 12 | updated_at, 13 | name, 14 | kana_name, 15 | group_name, 16 | kana_group_name, 17 | description, 18 | category, 19 | attributes, 20 | exceptional_complete_deadline, 21 | } = pending_project; 22 | 23 | sqlx::query!( 24 | r#" 25 | INSERT INTO pending_projects ( 26 | id, 27 | created_at, 28 | updated_at, 29 | name, 30 | kana_name, 31 | group_name, 32 | kana_group_name, 33 | description, 34 | category, 35 | attributes, 36 | exceptional_complete_deadline 37 | ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 , $11) 38 | "#, 39 | id, 40 | created_at, 41 | updated_at, 42 | name, 43 | kana_name, 44 | group_name, 45 | kana_group_name, 46 | description, 47 | category as _, 48 | attributes as _, 49 | exceptional_complete_deadline 50 | ) 51 | .execute(conn) 52 | .await 53 | .context("Failed to insert to pending projects")?; 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::Project; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_project<'a, E>(conn: E, project: Project) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let Project { 10 | id, 11 | index, 12 | created_at, 13 | updated_at, 14 | name, 15 | kana_name, 16 | group_name, 17 | kana_group_name, 18 | description, 19 | category, 20 | attributes, 21 | } = project; 22 | 23 | sqlx::query!( 24 | r#" 25 | INSERT INTO projects ( 26 | id, 27 | index, 28 | created_at, 29 | updated_at, 30 | name, 31 | kana_name, 32 | group_name, 33 | kana_group_name, 34 | description, 35 | category, 36 | attributes 37 | ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 ) 38 | "#, 39 | id, 40 | index, 41 | created_at, 42 | updated_at, 43 | name, 44 | kana_name, 45 | group_name, 46 | kana_group_name, 47 | description, 48 | category as _, 49 | attributes as _ 50 | ) 51 | .execute(conn) 52 | .await 53 | .context("Failed to insert to projects")?; 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_registration_form.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form::RegistrationForm; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_registration_form<'a, E>( 6 | conn: E, 7 | registration_form: RegistrationForm, 8 | ) -> Result<()> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | let RegistrationForm { 13 | id, 14 | created_at, 15 | author_id, 16 | name, 17 | description, 18 | items, 19 | } = registration_form; 20 | 21 | sqlx::query!( 22 | r#" 23 | INSERT INTO registration_forms ( 24 | id, 25 | created_at, 26 | author_id, 27 | name, 28 | description, 29 | items 30 | ) VALUES ( $1, $2, $3, $4, $5, $6 ) 31 | "#, 32 | id, 33 | created_at, 34 | author_id, 35 | name, 36 | description, 37 | items, 38 | ) 39 | .execute(conn) 40 | .await 41 | .context("Failed to insert to registration forms")?; 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_registration_form_answer.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form_answer::RegistrationFormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_registration_form_answer<'a, E>( 6 | conn: E, 7 | answer: RegistrationFormAnswer, 8 | ) -> Result<()> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | let RegistrationFormAnswer { 13 | id, 14 | created_at, 15 | updated_at, 16 | author_id, 17 | registration_form_id, 18 | project_id, 19 | pending_project_id, 20 | items, 21 | } = answer; 22 | 23 | sqlx::query!( 24 | r#" 25 | INSERT INTO registration_form_answers ( 26 | id, 27 | created_at, 28 | updated_at, 29 | author_id, 30 | registration_form_id, 31 | project_id, 32 | pending_project_id, 33 | items 34 | ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8 ) 35 | "#, 36 | id, 37 | created_at, 38 | updated_at, 39 | author_id, 40 | registration_form_id, 41 | project_id, 42 | pending_project_id, 43 | items, 44 | ) 45 | .execute(conn) 46 | .await 47 | .context("Failed to insert to registration form answers")?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /sos21-database/src/command/insert_user_invitation.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user_invitation::UserInvitation; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn insert_user_invitation<'a, E>(conn: E, invitation: UserInvitation) -> Result<()> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let UserInvitation { 10 | id, 11 | created_at, 12 | author_id, 13 | email, 14 | role, 15 | } = invitation; 16 | 17 | sqlx::query!( 18 | r#" 19 | INSERT INTO user_invitations ( 20 | id, 21 | created_at, 22 | author_id, 23 | email, 24 | role 25 | ) VALUES ( $1, $2, $3, $4, $5 ) 26 | "#, 27 | id, 28 | created_at, 29 | author_id, 30 | email, 31 | role as _ 32 | ) 33 | .execute(conn) 34 | .await 35 | .context("Failed to insert to user invitations")?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_file.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Input { 6 | pub id: Uuid, 7 | pub object_id: Uuid, 8 | pub blake3_digest: Vec, 9 | pub name: Option, 10 | pub type_: String, 11 | pub size: i64, 12 | } 13 | 14 | pub async fn update_file<'a, E>(conn: E, input: Input) -> Result<()> 15 | where 16 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 17 | { 18 | sqlx::query!( 19 | r#" 20 | UPDATE files 21 | SET 22 | object_id = $2, 23 | blake3_digest = $3, 24 | name = $4, 25 | type_ = $5, 26 | size = $6 27 | WHERE id = $1 28 | "#, 29 | input.id, 30 | input.object_id, 31 | input.blake3_digest, 32 | input.name, 33 | input.type_, 34 | input.size, 35 | ) 36 | .execute(conn) 37 | .await 38 | .context("Failed to update on files")?; 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_file_distribution.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Input { 6 | pub id: Uuid, 7 | pub name: String, 8 | pub description: String, 9 | } 10 | 11 | pub async fn update_file_distribution<'a, E>(conn: E, input: Input) -> Result<()> 12 | where 13 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 14 | { 15 | sqlx::query!( 16 | r#" 17 | UPDATE file_distributions 18 | SET 19 | name = $2, 20 | description = $3 21 | WHERE id = $1 22 | "#, 23 | input.id, 24 | input.name, 25 | input.description 26 | ) 27 | .execute(conn) 28 | .await 29 | .context("Failed to update on file distributions")?; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_form.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use chrono::{DateTime, Utc}; 3 | use uuid::Uuid; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Input { 7 | pub id: Uuid, 8 | pub name: String, 9 | pub description: String, 10 | pub starts_at: DateTime, 11 | pub ends_at: DateTime, 12 | pub items: serde_json::Value, 13 | } 14 | 15 | pub async fn update_form<'a, E>(conn: E, input: Input) -> Result<()> 16 | where 17 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 18 | { 19 | sqlx::query!( 20 | r#" 21 | UPDATE forms 22 | SET 23 | name = $2, 24 | description = $3, 25 | starts_at = $4, 26 | ends_at = $5, 27 | items = $6 28 | WHERE id = $1 29 | "#, 30 | input.id, 31 | input.name, 32 | input.description, 33 | input.starts_at, 34 | input.ends_at, 35 | input.items, 36 | ) 37 | .execute(conn) 38 | .await 39 | .context("Failed to update on forms")?; 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_form_answer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Input { 6 | pub id: Uuid, 7 | pub items: serde_json::Value, 8 | } 9 | 10 | pub async fn update_form_answer<'a, E>(conn: E, input: Input) -> Result<()> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 13 | { 14 | sqlx::query!( 15 | r#" 16 | UPDATE form_answers 17 | SET 18 | items = $2 19 | WHERE id = $1 20 | "#, 21 | input.id, 22 | input.items 23 | ) 24 | .execute(conn) 25 | .await 26 | .context("Failed to update on form answers")?; 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::{ProjectAttributes, ProjectCategory}; 2 | 3 | use anyhow::{Context, Result}; 4 | use chrono::{DateTime, Utc}; 5 | use uuid::Uuid; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Input { 9 | pub id: Uuid, 10 | pub updated_at: DateTime, 11 | pub name: String, 12 | pub kana_name: String, 13 | pub group_name: String, 14 | pub kana_group_name: String, 15 | pub description: String, 16 | pub category: ProjectCategory, 17 | pub attributes: ProjectAttributes, 18 | } 19 | 20 | pub async fn update_project<'a, E>(conn: E, input: Input) -> Result<()> 21 | where 22 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 23 | { 24 | sqlx::query!( 25 | r#" 26 | UPDATE projects 27 | SET 28 | name = $2, 29 | kana_name = $3, 30 | group_name = $4, 31 | kana_group_name = $5, 32 | description = $6, 33 | category = $7, 34 | attributes = $8, 35 | updated_at = $9 36 | WHERE id = $1 37 | "#, 38 | input.id, 39 | input.name, 40 | input.kana_name, 41 | input.group_name, 42 | input.kana_group_name, 43 | input.description, 44 | input.category as _, 45 | input.attributes as _, 46 | input.updated_at 47 | ) 48 | .execute(conn) 49 | .await 50 | .context("Failed to update on projects")?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_registration_form.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Input { 6 | pub id: Uuid, 7 | pub name: String, 8 | pub description: String, 9 | pub items: serde_json::Value, 10 | } 11 | 12 | pub async fn update_registration_form<'a, E>(conn: E, input: Input) -> Result<()> 13 | where 14 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 15 | { 16 | sqlx::query!( 17 | r#" 18 | UPDATE registration_forms 19 | SET 20 | name = $2, 21 | description = $3, 22 | items = $4 23 | WHERE id = $1 24 | "#, 25 | input.id, 26 | input.name, 27 | input.description, 28 | input.items, 29 | ) 30 | .execute(conn) 31 | .await 32 | .context("Failed to update on registration forms")?; 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_registration_form_answer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use chrono::{DateTime, Utc}; 3 | use uuid::Uuid; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Input { 7 | pub id: Uuid, 8 | pub updated_at: DateTime, 9 | pub project_id: Option, 10 | pub pending_project_id: Option, 11 | pub items: serde_json::Value, 12 | } 13 | 14 | pub async fn update_registration_form_answer<'a, E>(conn: E, input: Input) -> Result<()> 15 | where 16 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 17 | { 18 | sqlx::query!( 19 | r#" 20 | UPDATE registration_form_answers 21 | SET 22 | updated_at = $2, 23 | project_id = $3, 24 | pending_project_id = $4, 25 | items = $5 26 | WHERE id = $1 27 | "#, 28 | input.id, 29 | input.updated_at, 30 | input.project_id, 31 | input.pending_project_id, 32 | input.items 33 | ) 34 | .execute(conn) 35 | .await 36 | .context("Failed to update on registration form answers")?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /sos21-database/src/command/update_user_invitation.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user_invitation::UserInvitationRole; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Input { 8 | pub id: Uuid, 9 | pub email: String, 10 | pub role: UserInvitationRole, 11 | } 12 | 13 | pub async fn update_user_invitation<'a, E>(conn: E, input: Input) -> Result<()> 14 | where 15 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 16 | { 17 | sqlx::query!( 18 | r#" 19 | UPDATE user_invitations 20 | SET 21 | email = $2, 22 | role = $3 23 | WHERE id = $1 24 | "#, 25 | input.id, 26 | input.email, 27 | input.role as _, 28 | ) 29 | .execute(conn) 30 | .await 31 | .context("Failed to update on user invitations")?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /sos21-database/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | pub mod command; 4 | pub mod model; 5 | pub mod query; 6 | 7 | pub async fn migrate<'a, A>(conn: A) -> Result<()> 8 | where 9 | A: sqlx::Acquire<'a, Database = sqlx::Postgres>, 10 | { 11 | sqlx::migrate!("./migrations") 12 | .run(conn) 13 | .await 14 | .context("Failed to run migrations") 15 | } 16 | -------------------------------------------------------------------------------- /sos21-database/src/model.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod file_distribution; 3 | pub mod file_sharing; 4 | pub mod form; 5 | pub mod form_answer; 6 | pub mod pending_project; 7 | pub mod project; 8 | pub mod registration_form; 9 | pub mod registration_form_answer; 10 | pub mod user; 11 | pub mod user_invitation; 12 | -------------------------------------------------------------------------------- /sos21-database/src/model/file.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::FromRow)] 5 | pub struct File { 6 | pub id: Uuid, 7 | pub created_at: DateTime, 8 | pub author_id: String, 9 | pub object_id: Uuid, 10 | pub blake3_digest: Vec, 11 | pub name: Option, 12 | pub type_: String, 13 | pub size: i64, 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/model/file_distribution.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::FromRow)] 5 | pub struct FileDistribution { 6 | pub id: Uuid, 7 | pub created_at: DateTime, 8 | pub author_id: String, 9 | pub name: String, 10 | pub description: String, 11 | } 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct FileDistributionFile { 15 | pub project_id: Uuid, 16 | pub sharing_id: Uuid, 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct FileDistributionData { 21 | pub distribution: FileDistribution, 22 | pub files: Vec, 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/model/file_sharing.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::Type)] 5 | #[sqlx(type_name = "file_sharing_scope")] 6 | #[sqlx(rename_all = "snake_case")] 7 | pub enum FileSharingScope { 8 | Project, 9 | ProjectQuery, 10 | FormAnswer, 11 | RegistrationFormAnswer, 12 | Committee, 13 | CommitteeOperator, 14 | Public, 15 | } 16 | 17 | #[derive(Debug, Clone, sqlx::FromRow)] 18 | pub struct FileSharing { 19 | pub id: Uuid, 20 | pub created_at: DateTime, 21 | pub file_id: Uuid, 22 | pub is_revoked: bool, 23 | pub expires_at: Option>, 24 | pub scope: FileSharingScope, 25 | pub project_id: Option, 26 | pub project_query: Option, 27 | pub form_answer_project_id: Option, 28 | pub form_answer_form_id: Option, 29 | pub registration_form_answer_project_id: Option, 30 | pub registration_form_answer_pending_project_id: Option, 31 | pub registration_form_answer_registration_form_id: Option, 32 | } 33 | -------------------------------------------------------------------------------- /sos21-database/src/model/form.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::{ProjectAttributes, ProjectCategory}; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug, Clone, sqlx::FromRow)] 7 | pub struct Form { 8 | pub id: Uuid, 9 | pub created_at: DateTime, 10 | pub author_id: String, 11 | pub name: String, 12 | pub description: String, 13 | pub starts_at: DateTime, 14 | pub ends_at: DateTime, 15 | pub items: serde_json::Value, 16 | pub answer_notification_webhook: Option, 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct FormData { 21 | pub form: Form, 22 | pub include_ids: Vec, 23 | pub exclude_ids: Vec, 24 | pub query: Vec, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct FormProjectQueryConjunction { 29 | pub category: Option, 30 | pub attributes: ProjectAttributes, 31 | } 32 | -------------------------------------------------------------------------------- /sos21-database/src/model/form_answer.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::FromRow)] 5 | pub struct FormAnswer { 6 | pub id: Uuid, 7 | pub created_at: DateTime, 8 | pub author_id: String, 9 | pub form_id: Uuid, 10 | pub project_id: Uuid, 11 | pub items: serde_json::Value, 12 | } 13 | -------------------------------------------------------------------------------- /sos21-database/src/model/pending_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::{ProjectAttributes, ProjectCategory}; 2 | use crate::model::user::User; 3 | 4 | use chrono::{DateTime, Utc}; 5 | use uuid::Uuid; 6 | 7 | #[derive(Debug, Clone, sqlx::FromRow)] 8 | pub struct PendingProject { 9 | pub id: Uuid, 10 | pub created_at: DateTime, 11 | pub updated_at: DateTime, 12 | pub name: String, 13 | pub kana_name: String, 14 | pub group_name: String, 15 | pub kana_group_name: String, 16 | pub description: String, 17 | pub category: ProjectCategory, 18 | pub attributes: ProjectAttributes, 19 | pub exceptional_complete_deadline: Option>, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct PendingProjectWithOwner { 24 | pub pending_project: PendingProject, 25 | pub owner: User, 26 | } 27 | -------------------------------------------------------------------------------- /sos21-database/src/model/registration_form.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::{ProjectAttributes, ProjectCategory}; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug, Clone, sqlx::FromRow)] 7 | pub struct RegistrationForm { 8 | pub id: Uuid, 9 | pub created_at: DateTime, 10 | pub author_id: String, 11 | pub name: String, 12 | pub description: String, 13 | pub items: serde_json::Value, 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct RegistrationFormData { 18 | pub registration_form: RegistrationForm, 19 | pub query: Vec, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct RegistrationFormProjectQueryConjunction { 24 | pub category: Option, 25 | pub attributes: ProjectAttributes, 26 | } 27 | -------------------------------------------------------------------------------- /sos21-database/src/model/registration_form_answer.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::FromRow)] 5 | pub struct RegistrationFormAnswer { 6 | pub id: Uuid, 7 | pub created_at: DateTime, 8 | pub updated_at: DateTime, 9 | pub author_id: String, 10 | pub registration_form_id: Uuid, 11 | pub project_id: Option, 12 | pub pending_project_id: Option, 13 | pub items: serde_json::Value, 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/model/user.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::Type)] 5 | #[sqlx(type_name = "user_role")] 6 | #[sqlx(rename_all = "snake_case")] 7 | pub enum UserRole { 8 | Administrator, 9 | CommitteeOperator, 10 | Committee, 11 | General, 12 | } 13 | 14 | #[derive(Debug, Clone, sqlx::Type)] 15 | #[sqlx(type_name = "user_category")] 16 | #[sqlx(rename_all = "snake_case")] 17 | pub enum UserCategory { 18 | UndergraduateStudent, 19 | GraduateStudent, 20 | AcademicStaff, 21 | } 22 | 23 | #[derive(Debug, Clone, sqlx::Type)] 24 | #[sqlx(type_name = "user_assignment")] 25 | #[sqlx(rename_all = "snake_case")] 26 | pub enum UserAssignment { 27 | ProjectOwner, 28 | ProjectSubowner, 29 | PendingProjectOwner, 30 | } 31 | 32 | #[derive(Debug, Clone, sqlx::FromRow)] 33 | pub struct User { 34 | pub id: String, 35 | pub created_at: DateTime, 36 | pub first_name: String, 37 | pub kana_first_name: String, 38 | pub last_name: String, 39 | pub kana_last_name: String, 40 | pub phone_number: String, 41 | pub email: String, 42 | pub role: UserRole, 43 | pub category: UserCategory, 44 | pub assignment: Option, 45 | pub assignment_owner_project_id: Option, 46 | pub assignment_subowner_project_id: Option, 47 | pub assignment_owner_pending_project_id: Option, 48 | } 49 | -------------------------------------------------------------------------------- /sos21-database/src/model/user_invitation.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, sqlx::Type)] 5 | #[sqlx(type_name = "user_invitation_role")] 6 | #[sqlx(rename_all = "snake_case")] 7 | pub enum UserInvitationRole { 8 | Administrator, 9 | CommitteeOperator, 10 | Committee, 11 | } 12 | 13 | #[derive(Debug, Clone, sqlx::FromRow)] 14 | pub struct UserInvitation { 15 | pub id: Uuid, 16 | pub created_at: DateTime, 17 | pub author_id: String, 18 | pub email: String, 19 | pub role: UserInvitationRole, 20 | } 21 | -------------------------------------------------------------------------------- /sos21-database/src/query/count_projects.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | // TODO: use lightweight way to count projects 6 | // still, this is ok for now because number of projects are 7 | // no more than 1000 as a result (although it is not a knowledge of this layer) 8 | pub async fn count_projects<'a, E>(conn: E) -> Result 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | { 12 | let count = sqlx::query_scalar!(r#"SELECT count(*) as "count!" FROM projects"#) 13 | .fetch_one(conn) 14 | .await 15 | .context("Failed to count projects")?; 16 | let count = count.try_into()?; 17 | Ok(count) 18 | } 19 | -------------------------------------------------------------------------------- /sos21-database/src/query/count_registration_form_answers_by_pending_project.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn count_registration_form_answers_by_pending_project<'a, E>( 7 | conn: E, 8 | pending_project_id: Uuid, 9 | ) -> Result 10 | where 11 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 12 | { 13 | let count = sqlx::query_scalar!( 14 | "SELECT count(*) FROM registration_form_answers WHERE pending_project_id = $1", 15 | pending_project_id 16 | ) 17 | .fetch_one(conn) 18 | .await 19 | .context("Failed to select from registration form answers")?; 20 | 21 | let count = count.unwrap_or(0).try_into()?; 22 | Ok(count) 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/query/count_registration_forms_by_pending_project.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn count_registration_forms_by_pending_project<'a, E>( 7 | conn: E, 8 | pending_project_id: Uuid, 9 | ) -> Result 10 | where 11 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 12 | { 13 | let count = sqlx::query_scalar!( 14 | r#" 15 | SELECT count(registration_forms.id) 16 | FROM registration_forms 17 | WHERE ( 18 | SELECT 19 | bool_or(( 20 | registration_form_project_query_conjunctions.category = pending_projects.category IS NOT FALSE 21 | AND registration_form_project_query_conjunctions.attributes | pending_projects.attributes = pending_projects.attributes 22 | )) 23 | FROM registration_form_project_query_conjunctions, pending_projects 24 | WHERE registration_form_project_query_conjunctions.registration_form_id = registration_forms.id AND pending_projects.id = $1 25 | ) 26 | "#, 27 | pending_project_id 28 | ) 29 | .fetch_one(conn) 30 | .await 31 | .context("Failed to count registration forms")?; 32 | 33 | let count = count.unwrap_or(0).try_into()?; 34 | Ok(count) 35 | } 36 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_file.rs: -------------------------------------------------------------------------------- 1 | use crate::model::file::File; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_file<'a, E>(conn: E, id: Uuid) -> Result> 7 | where 8 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 9 | { 10 | sqlx::query_as!(File, "SELECT * FROM files WHERE id = $1", id) 11 | .fetch_optional(conn) 12 | .await 13 | .context("Failed to select from files") 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_form_answer.rs: -------------------------------------------------------------------------------- 1 | use crate::model::form_answer::FormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_form_answer<'a, E>(conn: E, id: Uuid) -> Result> 7 | where 8 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 9 | { 10 | sqlx::query_as!(FormAnswer, "SELECT * FROM form_answers WHERE id = $1", id) 11 | .fetch_optional(conn) 12 | .await 13 | .context("Failed to select from form answers") 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_form_answer_by_form_and_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::form_answer::FormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_form_answer_by_form_and_project<'a, E>( 7 | conn: E, 8 | form_id: Uuid, 9 | project_id: Uuid, 10 | ) -> Result> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 13 | { 14 | sqlx::query_as!( 15 | FormAnswer, 16 | "SELECT * FROM form_answers WHERE form_id = $1 AND project_id = $2", 17 | form_id, 18 | project_id 19 | ) 20 | .fetch_optional(conn) 21 | .await 22 | .context("Failed to select from form answers") 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_registration_form_answer.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form_answer::RegistrationFormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_registration_form_answer<'a, E>( 7 | conn: E, 8 | id: Uuid, 9 | ) -> Result> 10 | where 11 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 12 | { 13 | sqlx::query_as!( 14 | RegistrationFormAnswer, 15 | "SELECT * FROM registration_form_answers WHERE id = $1", 16 | id 17 | ) 18 | .fetch_optional(conn) 19 | .await 20 | .context("Failed to select from registration form answers") 21 | } 22 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_registration_form_answer_by_registration_form_and_pending_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form_answer::RegistrationFormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_registration_form_answer_by_registration_form_and_pending_project<'a, E>( 7 | conn: E, 8 | registration_form_id: Uuid, 9 | pending_project_id: Uuid, 10 | ) -> Result> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 13 | { 14 | sqlx::query_as!( 15 | RegistrationFormAnswer, 16 | "SELECT * FROM registration_form_answers WHERE registration_form_id = $1 AND pending_project_id = $2", 17 | registration_form_id, 18 | pending_project_id 19 | ) 20 | .fetch_optional(conn) 21 | .await 22 | .context("Failed to select from registration form answers") 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_registration_form_answer_by_registration_form_and_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form_answer::RegistrationFormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_registration_form_answer_by_registration_form_and_project<'a, E>( 7 | conn: E, 8 | registration_form_id: Uuid, 9 | project_id: Uuid, 10 | ) -> Result> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 13 | { 14 | sqlx::query_as!( 15 | RegistrationFormAnswer, 16 | "SELECT * FROM registration_form_answers WHERE registration_form_id = $1 AND project_id = $2", 17 | registration_form_id, 18 | project_id 19 | ) 20 | .fetch_optional(conn) 21 | .await 22 | .context("Failed to select from registration form answers") 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_user.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user::User; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn find_user<'a, E>(conn: E, id: String) -> Result> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | sqlx::query_as_unchecked!(User, "SELECT * FROM users WHERE id = $1", id) 10 | .fetch_optional(conn) 11 | .await 12 | .context("Failed to select from users") 13 | } 14 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_user_by_email.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user::User; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn find_user_by_email<'a, E, S>(conn: E, email: S) -> Result> 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | S: AsRef, 9 | { 10 | sqlx::query_as_unchecked!(User, "SELECT * FROM users WHERE email = $1", email.as_ref()) 11 | .fetch_optional(conn) 12 | .await 13 | .context("Failed to select from users") 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_user_invitation.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user_invitation::UserInvitation; 2 | 3 | use anyhow::{Context, Result}; 4 | use uuid::Uuid; 5 | 6 | pub async fn find_user_invitation<'a, E>(conn: E, id: Uuid) -> Result> 7 | where 8 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 9 | { 10 | sqlx::query_as_unchecked!( 11 | UserInvitation, 12 | "SELECT * FROM user_invitations WHERE id = $1", 13 | id 14 | ) 15 | .fetch_optional(conn) 16 | .await 17 | .context("Failed to select from user invitations") 18 | } 19 | -------------------------------------------------------------------------------- /sos21-database/src/query/find_user_invitation_by_email.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user_invitation::UserInvitation; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn find_user_invitation_by_email<'a, E, S>( 6 | conn: E, 7 | email: S, 8 | ) -> Result> 9 | where 10 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 11 | S: AsRef, 12 | { 13 | sqlx::query_as_unchecked!( 14 | UserInvitation, 15 | "SELECT * FROM user_invitations WHERE email = $1", 16 | email.as_ref(), 17 | ) 18 | .fetch_optional(conn) 19 | .await 20 | .context("Failed to select from user invitations") 21 | } 22 | -------------------------------------------------------------------------------- /sos21-database/src/query/get_next_index.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn get_next_index<'a, E>(conn: E) -> Result 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let index = sqlx::query_scalar!(r#"SELECT max(index)+1 as "index" FROM projects"#) 10 | .fetch_one(conn) 11 | .await 12 | .context("Failed to get next index")?; 13 | 14 | let index = match index { 15 | Some(x) => x.try_into()?, 16 | None => 0, 17 | }; 18 | 19 | Ok(index) 20 | } 21 | -------------------------------------------------------------------------------- /sos21-database/src/query/is_healthy.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | pub async fn is_healthy<'a, E>(conn: E) -> Result 6 | where 7 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 8 | { 9 | let table_names: &[&str] = [ 10 | "users", 11 | "projects", 12 | "forms", 13 | "form_condition_includes", 14 | "form_condition_excludes", 15 | "form_project_query_conjunctions", 16 | "form_answers", 17 | "file_sharings", 18 | "files", 19 | "file_distributions", 20 | "file_distribution_files", 21 | "pending_projects", 22 | ] 23 | .as_ref(); 24 | 25 | let table_names_len: i64 = table_names.len().try_into()?; 26 | 27 | let has_grants = sqlx::query_scalar!( 28 | r#" 29 | WITH grants AS ( 30 | SELECT 31 | array_agg(privilege_type::text) AS privilege_types, 32 | table_name::text 33 | FROM information_schema.role_table_grants 34 | WHERE grantee = current_user AND table_name::text = ANY ($1) 35 | GROUP BY table_name 36 | ) 37 | SELECT 38 | (bool_and(grants.privilege_types @> ARRAY['DELETE', 'UPDATE', 'SELECT', 'INSERT']) 39 | AND count(grants.table_name) = $2 40 | ) AS "has_grants!" 41 | FROM grants 42 | "#, 43 | table_names as _, 44 | table_names_len 45 | ) 46 | .fetch_one(conn) 47 | .await 48 | .context("Failed to select from role_table_grants")?; 49 | 50 | Ok(has_grants) 51 | } 52 | -------------------------------------------------------------------------------- /sos21-database/src/query/list_files_by_user.rs: -------------------------------------------------------------------------------- 1 | use crate::model::file::File; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::stream::{BoxStream, StreamExt}; 5 | 6 | pub fn list_files_by_user<'a, 'b, E>(conn: E, user_id: String) -> BoxStream<'b, Result> 7 | where 8 | E: sqlx::Executor<'a, Database = sqlx::Postgres> + 'b, 9 | 'a: 'b, 10 | { 11 | sqlx::query_as!(File, "SELECT * FROM files WHERE author_id = $1", user_id) 12 | .fetch(conn) 13 | .map(|result| result.context("Failed to select from files")) 14 | .boxed() 15 | } 16 | -------------------------------------------------------------------------------- /sos21-database/src/query/list_form_answers_by_form.rs: -------------------------------------------------------------------------------- 1 | use crate::model::form_answer::FormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::stream::{BoxStream, StreamExt}; 5 | use uuid::Uuid; 6 | 7 | pub fn list_form_answers_by_form<'a, E>(conn: E, form_id: Uuid) -> BoxStream<'a, Result> 8 | where 9 | E: sqlx::Executor<'a, Database = sqlx::Postgres> + 'a, 10 | { 11 | sqlx::query_as!( 12 | FormAnswer, 13 | "SELECT * FROM form_answers WHERE form_id = $1", 14 | form_id 15 | ) 16 | .fetch(conn) 17 | .map(|result| result.context("Failed to select from form_answers")) 18 | .boxed() 19 | } 20 | -------------------------------------------------------------------------------- /sos21-database/src/query/list_registration_form_answers_by_pending_project.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form_answer::RegistrationFormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::stream::{BoxStream, StreamExt}; 5 | use uuid::Uuid; 6 | 7 | pub fn list_registration_form_answers_by_pending_project<'a, 'b, E>( 8 | conn: E, 9 | pending_project_id: Uuid, 10 | ) -> BoxStream<'b, Result> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres> + 'b, 13 | 'a: 'b, 14 | { 15 | sqlx::query_as!( 16 | RegistrationFormAnswer, 17 | "SELECT * FROM registration_form_answers WHERE pending_project_id = $1", 18 | pending_project_id 19 | ) 20 | .fetch(conn) 21 | .map(|result| result.context("Failed to select from registration form answers")) 22 | .boxed() 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/query/list_registration_form_answers_by_registration_form.rs: -------------------------------------------------------------------------------- 1 | use crate::model::registration_form_answer::RegistrationFormAnswer; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::stream::{BoxStream, StreamExt}; 5 | use uuid::Uuid; 6 | 7 | pub fn list_registration_form_answers_by_registration_form<'a, 'b, E>( 8 | conn: E, 9 | registration_form_id: Uuid, 10 | ) -> BoxStream<'b, Result> 11 | where 12 | E: sqlx::Executor<'a, Database = sqlx::Postgres> + 'b, 13 | 'a: 'b, 14 | { 15 | sqlx::query_as!( 16 | RegistrationFormAnswer, 17 | "SELECT * FROM registration_form_answers WHERE registration_form_id = $1", 18 | registration_form_id 19 | ) 20 | .fetch(conn) 21 | .map(|result| result.context("Failed to select from registration form answers")) 22 | .boxed() 23 | } 24 | -------------------------------------------------------------------------------- /sos21-database/src/query/list_user_invitations.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user_invitation::UserInvitation; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::stream::{BoxStream, StreamExt}; 5 | 6 | pub fn list_user_invitations<'a, E>(conn: E) -> BoxStream<'a, Result> 7 | where 8 | E: sqlx::Executor<'a, Database = sqlx::Postgres> + 'a, 9 | { 10 | sqlx::query_as_unchecked!(UserInvitation, "SELECT * FROM user_invitations") 11 | .fetch(conn) 12 | .map(|result| result.context("Failed to select from user invitations")) 13 | .boxed() 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/query/list_users.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user::User; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::stream::{BoxStream, StreamExt}; 5 | 6 | pub fn list_users<'a, E>(conn: E) -> BoxStream<'a, Result> 7 | where 8 | E: sqlx::Executor<'a, Database = sqlx::Postgres> + 'a, 9 | { 10 | sqlx::query_as_unchecked!(User, "SELECT * FROM users") 11 | .fetch(conn) 12 | .map(|result| result.context("Failed to select from users")) 13 | .boxed() 14 | } 15 | -------------------------------------------------------------------------------- /sos21-database/src/query/sum_file_size_by_user.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | pub async fn sum_file_size_by_user<'a, E>(conn: E, author_id: String) -> Result 4 | where 5 | E: sqlx::Executor<'a, Database = sqlx::Postgres>, 6 | { 7 | sqlx::query_scalar!( 8 | "SELECT sum(size)::bigint FROM files WHERE author_id = $1", 9 | author_id 10 | ) 11 | .fetch_one(conn) 12 | .await 13 | .context("Failed to select from files") 14 | .map(|opt| opt.unwrap_or(0)) 15 | } 16 | -------------------------------------------------------------------------------- /sos21-domain/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-domain" 3 | version = "0.7.1" 4 | authors = ["coord_e ", "azarashi2931 ", "yuseiito ", "momeemt "] 5 | edition = "2018" 6 | readme = "README.md" 7 | documentation = "https://sohosai.github.io/sos21-backend/sos21_domain/" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [features] 11 | # TODO: get rid of this confusing flag (related: rust-lang/rust#79381) 12 | test = ["tokio", "maplit", "once_cell", "rand"] 13 | 14 | [dependencies] 15 | chrono = { version = "0.4", features = ["serde"] } 16 | thiserror = "1" 17 | enumflags2 = "0.7" 18 | paste = "1" 19 | uuid = { version = "0.8", features = ["serde", "v4"] } 20 | typenum = "1" 21 | async-trait = "0.1.42" 22 | anyhow = "1" 23 | serde = { version = "1", features = ["derive"] } 24 | bytes = "1" 25 | mime = "0.3" 26 | futures = "0.3" 27 | blake3 = "0.3" 28 | unicode-segmentation = "1.7" 29 | num-rational = "0.4" 30 | auto_enums = "0.7" 31 | 32 | # these optional dependencies are used by `test` feature (which is enabled for the use from other crates) 33 | tokio = { version = "1", optional = true, features = ["macros", "rt-multi-thread"] } 34 | maplit = { version = "1", optional = true } 35 | once_cell = { version = "1", optional = true } 36 | rand = { version = "0.8", optional = true } 37 | 38 | [dev-dependencies] 39 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 40 | maplit = "1" 41 | once_cell = "1" 42 | rand = "0.8" 43 | 44 | [build-dependencies] 45 | syn = "1" -------------------------------------------------------------------------------- /sos21-domain/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-domain` 2 | 3 | [![docs (develop)](https://img.shields.io/badge/docs-develop-blue)](https://sohosai.github.io/sos21-backend/develop/sos21_domain/) 4 | [![docs](https://img.shields.io/github/v/release/sohosai/sos21-backend?label=docs&color=blue)](https://sohosai.github.io/sos21-backend/sos21_domain/) 5 | 6 | ドメイン層。ドメインモデルと永続化のためのインターフェースを記述します。 7 | -------------------------------------------------------------------------------- /sos21-domain/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-domain 8 | -------------------------------------------------------------------------------- /sos21-domain/src/context.rs: -------------------------------------------------------------------------------- 1 | pub mod authentication; 2 | pub mod login; 3 | pub use authentication::Authentication; 4 | pub use login::Login; 5 | 6 | pub mod config; 7 | pub mod file_distribution_repository; 8 | pub mod file_repository; 9 | pub mod file_sharing_repository; 10 | pub mod form_answer_repository; 11 | pub mod form_repository; 12 | pub mod object_repository; 13 | pub mod pending_project_repository; 14 | pub mod project_repository; 15 | pub mod registration_form_answer_repository; 16 | pub mod registration_form_repository; 17 | pub mod user_invitation_repository; 18 | pub mod user_repository; 19 | pub use config::ConfigContext; 20 | pub use file_distribution_repository::FileDistributionRepository; 21 | pub use file_repository::FileRepository; 22 | pub use file_sharing_repository::FileSharingRepository; 23 | pub use form_answer_repository::FormAnswerRepository; 24 | pub use form_repository::FormRepository; 25 | pub use object_repository::ObjectRepository; 26 | pub use pending_project_repository::PendingProjectRepository; 27 | pub use project_repository::ProjectRepository; 28 | pub use registration_form_answer_repository::RegistrationFormAnswerRepository; 29 | pub use registration_form_repository::RegistrationFormRepository; 30 | pub use user_invitation_repository::UserInvitationRepository; 31 | pub use user_repository::UserRepository; 32 | -------------------------------------------------------------------------------- /sos21-domain/src/context/config.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::ProjectCategory; 2 | use crate::model::project_creation_period::ProjectCreationPeriod; 3 | use crate::model::user::UserEmailAddress; 4 | 5 | pub trait ConfigContext { 6 | fn administrator_email(&self) -> &UserEmailAddress; 7 | fn project_creation_period_for(&self, category: ProjectCategory) -> ProjectCreationPeriod; 8 | } 9 | 10 | #[macro_export] 11 | macro_rules! delegate_config_context { 12 | (impl $(<$($vars:ident $(: $c0:ident $(+ $cs:ident)* )? ),*>)? ConfigContext for $ty:ty { 13 | $sel:ident $target:block 14 | }) => { 15 | impl $(<$($vars$(: $c0 $(+ $cs)* )?,)*>)? $crate::context::ConfigContext for $ty { 16 | fn administrator_email(&$sel) -> &$crate::model::user::UserEmailAddress { 17 | $target.administrator_email() 18 | } 19 | fn project_creation_period_for( 20 | &$sel, 21 | category: $crate::model::project::ProjectCategory, 22 | ) -> $crate::model::project_creation_period::ProjectCreationPeriod { 23 | $target.project_creation_period_for(category) 24 | } 25 | } 26 | } 27 | } 28 | 29 | impl ConfigContext for &C { 30 | fn administrator_email(&self) -> &UserEmailAddress { 31 | ::administrator_email(self) 32 | } 33 | 34 | fn project_creation_period_for(&self, category: ProjectCategory) -> ProjectCreationPeriod { 35 | ::project_creation_period_for(self, category) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sos21-domain/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum DomainError { 3 | Domain(E), 4 | Internal(anyhow::Error), 5 | } 6 | 7 | impl DomainError { 8 | pub fn map_domain(self, op: F) -> DomainError 9 | where 10 | F: FnOnce(T) -> U, 11 | { 12 | match self { 13 | DomainError::Domain(err) => DomainError::Domain(op(err)), 14 | DomainError::Internal(err) => DomainError::Internal(err), 15 | } 16 | } 17 | } 18 | 19 | impl From for DomainError { 20 | fn from(e: anyhow::Error) -> Self { 21 | DomainError::Internal(e) 22 | } 23 | } 24 | 25 | pub type DomainResult = Result>; 26 | -------------------------------------------------------------------------------- /sos21-domain/src/lib.rs: -------------------------------------------------------------------------------- 1 | macro_rules! domain_ensure { 2 | ($cond:expr) => { 3 | if !$cond { 4 | return Err($crate::DomainError::Internal(::anyhow::anyhow!(concat!( 5 | "Condition failed: `", 6 | stringify!($cond), 7 | "`" 8 | )))); 9 | } 10 | }; 11 | } 12 | 13 | pub mod context; 14 | pub mod model; 15 | 16 | mod error; 17 | pub use error::{DomainError, DomainResult}; 18 | 19 | #[cfg(any(feature = "test", test))] 20 | pub mod test; 21 | -------------------------------------------------------------------------------- /sos21-domain/src/model.rs: -------------------------------------------------------------------------------- 1 | mod bound; 2 | mod collection; 3 | mod integer; 4 | mod string; 5 | 6 | pub mod date_time; 7 | pub mod email; 8 | pub mod file; 9 | pub mod file_distribution; 10 | pub mod file_sharing; 11 | pub mod form; 12 | pub mod form_answer; 13 | pub mod object; 14 | pub mod pending_project; 15 | pub mod permissions; 16 | pub mod phone_number; 17 | pub mod project; 18 | pub mod project_creation_period; 19 | pub mod project_query; 20 | pub mod registration_form; 21 | pub mod registration_form_answer; 22 | pub mod user; 23 | pub mod user_invitation; 24 | -------------------------------------------------------------------------------- /sos21-domain/src/model/bound.rs: -------------------------------------------------------------------------------- 1 | use typenum::{Integer, Unsigned}; 2 | 3 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 4 | pub struct Bounded(N); 5 | 6 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 7 | pub struct Unbounded; 8 | 9 | pub trait Bound { 10 | fn limit() -> Option; 11 | } 12 | 13 | impl Bound for Unbounded { 14 | fn limit() -> Option { 15 | None 16 | } 17 | } 18 | 19 | impl Bound for Bounded { 20 | fn limit() -> Option { 21 | Some(N::to_usize()) 22 | } 23 | } 24 | 25 | impl Bound for Bounded { 26 | fn limit() -> Option { 27 | Some(N::to_u64()) 28 | } 29 | } 30 | 31 | impl Bound for Bounded { 32 | fn limit() -> Option { 33 | Some(N::to_u16()) 34 | } 35 | } 36 | 37 | impl Bound for Bounded { 38 | fn limit() -> Option { 39 | Some(N::to_i64()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sos21-domain/src/model/date_time.rs: -------------------------------------------------------------------------------- 1 | /// A point of time without timezone-related semantics. 2 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 3 | pub struct DateTime(chrono::DateTime); 4 | 5 | impl DateTime { 6 | pub fn now() -> Self { 7 | Self::from_utc(chrono::Utc::now()) 8 | } 9 | 10 | pub fn from_utc(utc: chrono::DateTime) -> Self { 11 | DateTime(utc) 12 | } 13 | 14 | pub fn utc(&self) -> chrono::DateTime { 15 | self.0 16 | } 17 | 18 | pub fn jst(&self) -> chrono::DateTime { 19 | // FIXME: 範囲外の場合のハンドリングをする 20 | let jst = chrono::FixedOffset::east_opt(9 * 3600).unwrap(); 21 | self.0.with_timezone(&jst) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file/digest.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct FileBlake3Digest([u8; 32]); 7 | 8 | #[derive(Debug, Clone, Error)] 9 | #[error("invalid length of blake3 digest bytes")] 10 | pub struct InvalidLengthError { 11 | _priv: (), 12 | } 13 | 14 | impl FileBlake3Digest { 15 | pub fn from_array(bytes: [u8; 32]) -> Self { 16 | FileBlake3Digest(bytes) 17 | } 18 | 19 | pub fn from_vec(bytes: Vec) -> Result { 20 | bytes 21 | .try_into() 22 | .map(FileBlake3Digest) 23 | .map_err(|_| InvalidLengthError { _priv: () }) 24 | } 25 | 26 | pub fn into_array(self) -> [u8; 32] { 27 | self.0 28 | } 29 | 30 | pub fn as_slice(&self) -> &[u8] { 31 | &self.0[..] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file/name.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::{self, LengthBoundedString}; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct FileName(LengthBoundedString); 7 | 8 | #[derive(Debug, Error, Clone)] 9 | #[error("invalid file name")] 10 | pub struct NameError { 11 | _priv: (), 12 | } 13 | 14 | impl NameError { 15 | fn from_length_error(_err: string::BoundedLengthError) -> Self { 16 | NameError { _priv: () } 17 | } 18 | } 19 | 20 | impl FileName { 21 | pub fn from_string(name: impl Into) -> Result { 22 | let inner = LengthBoundedString::new(name.into()).map_err(NameError::from_length_error)?; 23 | Ok(FileName(inner)) 24 | } 25 | 26 | pub fn as_str(&self) -> &str { 27 | self.0.as_ref() 28 | } 29 | 30 | pub fn into_string(self) -> String { 31 | self.0.into_inner() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file/size.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 2 | pub struct FileSize(u64); 3 | 4 | impl FileSize { 5 | pub fn from_number_of_bytes(size: u64) -> Self { 6 | FileSize(size) 7 | } 8 | 9 | pub fn to_number_of_bytes(&self) -> u64 { 10 | self.0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file/type_.rs: -------------------------------------------------------------------------------- 1 | use mime::Mime; 2 | use serde::{ 3 | de::{self, Deserialize, Deserializer}, 4 | ser::{Serialize, Serializer}, 5 | }; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 8 | pub struct FileType(Mime); 9 | 10 | impl FileType { 11 | pub fn from_mime(mime: Mime) -> Self { 12 | FileType(mime) 13 | } 14 | 15 | pub fn as_mime(&self) -> &'_ Mime { 16 | &self.0 17 | } 18 | 19 | pub fn into_mime(self) -> Mime { 20 | self.0 21 | } 22 | } 23 | 24 | impl Default for FileType { 25 | fn default() -> Self { 26 | FileType::from_mime(mime::APPLICATION_OCTET_STREAM) 27 | } 28 | } 29 | 30 | impl Serialize for FileType { 31 | fn serialize(&self, serializer: S) -> Result 32 | where 33 | S: Serializer, 34 | { 35 | serializer.collect_str(self.as_mime()) 36 | } 37 | } 38 | 39 | impl<'de> Deserialize<'de> for FileType { 40 | fn deserialize(deserializer: D) -> Result 41 | where 42 | D: Deserializer<'de>, 43 | { 44 | String::deserialize(deserializer)? 45 | .parse() 46 | .map(FileType::from_mime) 47 | .map_err(de::Error::custom) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file_distribution/description.rs: -------------------------------------------------------------------------------- 1 | use crate::model::bound::{Bounded, Unbounded}; 2 | use crate::model::string::{self, LengthLimitedString, StrippedString}; 3 | 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct FileDistributionDescription( 8 | StrippedString, String>>, 9 | ); 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid file distribution description")] 13 | pub struct DescriptionError { 14 | _priv: (), 15 | } 16 | 17 | impl DescriptionError { 18 | fn from_length_error(_err: string::LengthError>) -> Self { 19 | DescriptionError { _priv: () } 20 | } 21 | 22 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 23 | DescriptionError { _priv: () } 24 | } 25 | } 26 | 27 | impl FileDistributionDescription { 28 | pub fn from_string(description: impl Into) -> Result { 29 | let inner = LengthLimitedString::new(description.into()) 30 | .map_err(DescriptionError::from_length_error)?; 31 | let inner = 32 | StrippedString::new(inner).map_err(DescriptionError::from_not_stripped_error)?; 33 | Ok(FileDistributionDescription(inner)) 34 | } 35 | 36 | pub fn as_str(&self) -> &str { 37 | self.0.as_str() 38 | } 39 | 40 | pub fn into_string(self) -> String { 41 | self.0.into_inner().into_inner() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file_distribution/name.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::{self, LengthBoundedString, StrippedString}; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct FileDistributionName( 7 | StrippedString>, 8 | ); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid file distribution name")] 12 | pub struct NameError { 13 | _priv: (), 14 | } 15 | 16 | impl NameError { 17 | fn from_length_error(_err: string::BoundedLengthError) -> Self { 18 | NameError { _priv: () } 19 | } 20 | 21 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 22 | NameError { _priv: () } 23 | } 24 | } 25 | 26 | impl FileDistributionName { 27 | pub fn from_string(name: impl Into) -> Result { 28 | let inner = LengthBoundedString::new(name.into()).map_err(NameError::from_length_error)?; 29 | let inner = StrippedString::new(inner).map_err(NameError::from_not_stripped_error)?; 30 | Ok(FileDistributionName(inner)) 31 | } 32 | 33 | pub fn as_str(&self) -> &str { 34 | self.0.as_str() 35 | } 36 | 37 | pub fn into_string(self) -> String { 38 | self.0.into_inner().into_inner() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sos21-domain/src/model/file_sharing/state.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub enum FileSharingState { 3 | Active, 4 | Revoked, 5 | Expired, 6 | } 7 | 8 | impl FileSharingState { 9 | pub fn is_active(&self) -> bool { 10 | matches!(self, FileSharingState::Active) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/description.rs: -------------------------------------------------------------------------------- 1 | use crate::model::bound::{Bounded, Unbounded}; 2 | use crate::model::string::{self, LengthLimitedString, StrippedString}; 3 | 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct FormDescription( 8 | StrippedString, String>>, 9 | ); 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid form description")] 13 | pub struct DescriptionError { 14 | _priv: (), 15 | } 16 | 17 | impl DescriptionError { 18 | fn from_length_error(_err: string::LengthError>) -> Self { 19 | DescriptionError { _priv: () } 20 | } 21 | 22 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 23 | DescriptionError { _priv: () } 24 | } 25 | } 26 | 27 | impl FormDescription { 28 | pub fn from_string(name: impl Into) -> Result { 29 | let inner = 30 | LengthLimitedString::new(name.into()).map_err(DescriptionError::from_length_error)?; 31 | let inner = 32 | StrippedString::new(inner).map_err(DescriptionError::from_not_stripped_error)?; 33 | Ok(FormDescription(inner)) 34 | } 35 | 36 | pub fn into_string(self) -> String { 37 | self.0.into_inner().into_inner() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/checkbox/limit.rs: -------------------------------------------------------------------------------- 1 | use crate::model::integer::BoundedInteger; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 7 | #[serde(transparent)] 8 | pub struct CheckboxFormItemLimit(BoundedInteger); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid checkbox form item limit")] 12 | pub struct LimitError { 13 | _priv: (), 14 | } 15 | 16 | impl CheckboxFormItemLimit { 17 | pub fn from_u64(limit: u64) -> Result { 18 | let inner = BoundedInteger::new(limit).map_err(|_| LimitError { _priv: () })?; 19 | Ok(CheckboxFormItemLimit(inner)) 20 | } 21 | 22 | pub fn to_u64(self) -> u64 { 23 | self.0.into_inner() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/description.rs: -------------------------------------------------------------------------------- 1 | use crate::model::bound::{Bounded, Unbounded}; 2 | use crate::model::string::{self, LengthLimitedString, StrippedString}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use thiserror::Error; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 8 | #[serde(transparent)] 9 | pub struct FormItemDescription( 10 | StrippedString, String>>, 11 | ); 12 | 13 | #[derive(Debug, Error, Clone)] 14 | #[error("invalid form item description")] 15 | pub struct ItemDescriptionError { 16 | _priv: (), 17 | } 18 | 19 | impl ItemDescriptionError { 20 | fn from_length_error(_err: string::LengthError>) -> Self { 21 | ItemDescriptionError { _priv: () } 22 | } 23 | 24 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 25 | ItemDescriptionError { _priv: () } 26 | } 27 | } 28 | 29 | impl FormItemDescription { 30 | pub fn from_string(name: impl Into) -> Result { 31 | let inner = LengthLimitedString::new(name.into()) 32 | .map_err(ItemDescriptionError::from_length_error)?; 33 | let inner = 34 | StrippedString::new(inner).map_err(ItemDescriptionError::from_not_stripped_error)?; 35 | Ok(FormItemDescription(inner)) 36 | } 37 | 38 | pub fn into_string(self) -> String { 39 | self.0.into_inner().into_inner() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/integer/limit.rs: -------------------------------------------------------------------------------- 1 | use crate::model::bound::{Bounded, Unbounded}; 2 | use crate::model::integer::LimitedInteger; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use thiserror::Error; 6 | 7 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 8 | #[serde(transparent)] 9 | pub struct IntegerFormItemLimit(LimitedInteger, Unbounded, u64>); 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid integer form item limit")] 13 | pub struct LimitError { 14 | _priv: (), 15 | } 16 | 17 | impl IntegerFormItemLimit { 18 | pub fn from_u64(limit: u64) -> Result { 19 | let inner = LimitedInteger::new(limit).map_err(|_| LimitError { _priv: () })?; 20 | Ok(IntegerFormItemLimit(inner)) 21 | } 22 | 23 | pub fn to_u64(self) -> u64 { 24 | self.0.into_inner() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/integer/unit.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::LengthBoundedString; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7 | #[serde(transparent)] 8 | pub struct IntegerFormItemUnit(LengthBoundedString); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid integer form item unit")] 12 | pub struct UnitError { 13 | _priv: (), 14 | } 15 | 16 | impl IntegerFormItemUnit { 17 | pub fn from_string(unit: impl Into) -> Result { 18 | let inner = LengthBoundedString::new(unit.into()).map_err(|_| UnitError { _priv: () })?; 19 | Ok(IntegerFormItemUnit(inner)) 20 | } 21 | 22 | pub fn into_string(self) -> String { 23 | self.0.into_inner() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/name.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::{self, LengthBoundedString, StrippedString}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7 | #[serde(transparent)] 8 | pub struct FormItemName(StrippedString>); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid form item name")] 12 | pub struct ItemNameError { 13 | _priv: (), 14 | } 15 | 16 | impl ItemNameError { 17 | fn from_length_error(_err: string::BoundedLengthError) -> Self { 18 | ItemNameError { _priv: () } 19 | } 20 | 21 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 22 | ItemNameError { _priv: () } 23 | } 24 | } 25 | 26 | impl FormItemName { 27 | pub fn from_string(name: impl Into) -> Result { 28 | let inner = 29 | LengthBoundedString::new(name.into()).map_err(ItemNameError::from_length_error)?; 30 | let inner = StrippedString::new(inner).map_err(ItemNameError::from_not_stripped_error)?; 31 | Ok(FormItemName(inner)) 32 | } 33 | 34 | pub fn as_str(&self) -> &str { 35 | self.0.as_str() 36 | } 37 | 38 | pub fn into_string(self) -> String { 39 | self.0.into_inner().into_inner() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/text/length.rs: -------------------------------------------------------------------------------- 1 | use crate::model::integer::BoundedInteger; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 7 | #[serde(transparent)] 8 | pub struct TextFormItemLength(BoundedInteger); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid text form item length")] 12 | pub struct LengthError { 13 | _priv: (), 14 | } 15 | 16 | impl TextFormItemLength { 17 | pub fn from_u64(length: u64) -> Result { 18 | let inner = BoundedInteger::new(length).map_err(|_| LengthError { _priv: () })?; 19 | Ok(TextFormItemLength(inner)) 20 | } 21 | 22 | pub fn to_u64(self) -> u64 { 23 | self.0.into_inner() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/item/text/placeholder.rs: -------------------------------------------------------------------------------- 1 | use crate::model::bound::{Bounded, Unbounded}; 2 | use crate::model::string::LengthLimitedString; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use thiserror::Error; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 8 | #[serde(transparent)] 9 | pub struct TextFormItemPlaceholder(LengthLimitedString, String>); 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid text form item placeholder")] 13 | pub struct PlaceholderError { 14 | _priv: (), 15 | } 16 | 17 | impl TextFormItemPlaceholder { 18 | pub fn from_string(name: impl Into) -> Result { 19 | let inner = 20 | LengthLimitedString::new(name.into()).map_err(|_| PlaceholderError { _priv: () })?; 21 | Ok(TextFormItemPlaceholder(inner)) 22 | } 23 | 24 | pub fn len(&self) -> usize { 25 | self.0.len() 26 | } 27 | 28 | pub fn is_empty(&self) -> bool { 29 | self.0.len() == 0 30 | } 31 | 32 | pub fn into_string(self) -> String { 33 | self.0.into_inner() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/name.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::{self, LengthBoundedString, StrippedString}; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct FormName(StrippedString>); 7 | 8 | #[derive(Debug, Error, Clone)] 9 | #[error("invalid form name")] 10 | pub struct NameError { 11 | _priv: (), 12 | } 13 | 14 | impl NameError { 15 | fn from_length_error(_err: string::BoundedLengthError) -> Self { 16 | NameError { _priv: () } 17 | } 18 | 19 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 20 | NameError { _priv: () } 21 | } 22 | } 23 | 24 | impl FormName { 25 | pub fn from_string(name: impl Into) -> Result { 26 | let inner = LengthBoundedString::new(name.into()).map_err(NameError::from_length_error)?; 27 | let inner = StrippedString::new(inner).map_err(NameError::from_not_stripped_error)?; 28 | Ok(FormName(inner)) 29 | } 30 | 31 | pub fn as_str(&self) -> &str { 32 | self.0.as_str() 33 | } 34 | 35 | pub fn into_string(self) -> String { 36 | self.0.into_inner().into_inner() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form/period.rs: -------------------------------------------------------------------------------- 1 | use crate::model::date_time::DateTime; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct FormPeriod { 7 | starts_at: DateTime, 8 | ends_at: DateTime, 9 | } 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid form period")] 13 | pub struct PeriodError { 14 | _priv: (), 15 | } 16 | 17 | impl FormPeriod { 18 | pub fn from_datetime(starts_at: DateTime, ends_at: DateTime) -> Result { 19 | if starts_at >= ends_at { 20 | return Err(PeriodError { _priv: () }); 21 | } 22 | 23 | Ok(FormPeriod { starts_at, ends_at }) 24 | } 25 | 26 | pub fn set_starts_at(&mut self, starts_at: DateTime) -> Result<(), PeriodError> { 27 | if starts_at >= self.ends_at { 28 | return Err(PeriodError { _priv: () }); 29 | } 30 | 31 | self.starts_at = starts_at; 32 | Ok(()) 33 | } 34 | 35 | pub fn set_ends_at(&mut self, ends_at: DateTime) -> Result<(), PeriodError> { 36 | if self.starts_at >= ends_at { 37 | return Err(PeriodError { _priv: () }); 38 | } 39 | 40 | self.ends_at = ends_at; 41 | Ok(()) 42 | } 43 | 44 | pub fn starts_at(&self) -> DateTime { 45 | self.starts_at 46 | } 47 | 48 | pub fn ends_at(&self) -> DateTime { 49 | self.ends_at 50 | } 51 | 52 | pub fn contains(&self, time: DateTime) -> bool { 53 | self.starts_at() <= time && self.ends_at() > time 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sos21-domain/src/model/form_answer/item/text.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::LengthBoundedString; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7 | #[serde(transparent)] 8 | pub struct FormAnswerItemText(LengthBoundedString); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid form answer item text")] 12 | pub struct TextError { 13 | _priv: (), 14 | } 15 | 16 | #[allow(clippy::len_without_is_empty)] 17 | impl FormAnswerItemText { 18 | pub fn from_string(text: impl Into) -> Result { 19 | let inner = LengthBoundedString::new(text.into()).map_err(|_| TextError { _priv: () })?; 20 | Ok(FormAnswerItemText(inner)) 21 | } 22 | 23 | pub fn contains_line_break(&self) -> bool { 24 | let s: &str = self.0.as_ref(); 25 | s.contains('\n') 26 | } 27 | 28 | /// it always stands that `text.len() > 0`. 29 | pub fn len(&self) -> usize { 30 | let len = self.0.len(); 31 | debug_assert!(len > 0); 32 | len 33 | } 34 | 35 | pub fn into_string(self) -> String { 36 | self.0.into_inner() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sos21-domain/src/model/object.rs: -------------------------------------------------------------------------------- 1 | use crate::model::file::File; 2 | use crate::model::file_sharing::FileSharingWitness; 3 | use crate::model::user::User; 4 | 5 | use uuid::Uuid; 6 | 7 | pub mod data; 8 | pub use data::ObjectData; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 11 | pub struct ObjectId(Uuid); 12 | 13 | impl ObjectId { 14 | pub fn from_uuid(uuid: Uuid) -> ObjectId { 15 | ObjectId(uuid) 16 | } 17 | 18 | pub fn to_uuid(&self) -> Uuid { 19 | self.0 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct Object { 25 | pub id: ObjectId, 26 | pub data: ObjectData, 27 | } 28 | 29 | impl Object { 30 | pub fn is_visible_to(&self, _user: &User) -> bool { 31 | false 32 | } 33 | 34 | pub fn is_visible_to_with_file(&self, user: &User, file: &File) -> bool { 35 | if self.is_visible_to(user) { 36 | return true; 37 | } 38 | 39 | self.id == file.object_id && file.is_visible_to(user) 40 | } 41 | 42 | pub fn is_visible_to_with_sharing(&self, file: &File, witness: &FileSharingWitness) -> bool { 43 | self.id == file.object_id && file.is_visible_to_with_sharing(witness) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sos21-domain/src/model/project/attribute.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] 7 | pub enum ProjectAttribute { 8 | Academic, 9 | Artistic, 10 | Committee, 11 | Outdoor, 12 | Indoor, 13 | } 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] 16 | pub struct ProjectAttributes(HashSet); 17 | 18 | #[derive(Debug, Error, Clone)] 19 | #[error("duplicated project attributes")] 20 | pub struct DuplicatedAttributesError { 21 | _priv: (), 22 | } 23 | 24 | impl ProjectAttributes { 25 | pub fn from_attributes(attributes: I) -> Result 26 | where 27 | I: IntoIterator, 28 | { 29 | let mut result = HashSet::new(); 30 | for attribute in attributes { 31 | if !result.insert(attribute) { 32 | return Err(DuplicatedAttributesError { _priv: () }); 33 | } 34 | } 35 | Ok(ProjectAttributes(result)) 36 | } 37 | 38 | pub fn contains(&self, attribute: ProjectAttribute) -> bool { 39 | self.0.contains(&attribute) 40 | } 41 | 42 | pub fn is_subset(&self, attributes: &ProjectAttributes) -> bool { 43 | self.0.is_subset(&attributes.0) 44 | } 45 | 46 | pub fn attributes(&self) -> impl Iterator + '_ { 47 | self.0.iter().copied() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sos21-domain/src/model/project/description.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::{self, LengthBoundedString, StrippedString}; 2 | 3 | use thiserror::Error; 4 | 5 | /// A description text of projects, whose length is 1 ..= 50 chars. 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct ProjectDescription( 8 | StrippedString>, 9 | ); 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid project description")] 13 | pub struct DescriptionError { 14 | _priv: (), 15 | } 16 | 17 | impl DescriptionError { 18 | fn from_length_error(_err: string::BoundedLengthError) -> Self { 19 | DescriptionError { _priv: () } 20 | } 21 | 22 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 23 | DescriptionError { _priv: () } 24 | } 25 | } 26 | 27 | impl ProjectDescription { 28 | pub fn from_string(description: impl Into) -> Result { 29 | let inner = LengthBoundedString::new(description.into()) 30 | .map_err(DescriptionError::from_length_error)?; 31 | let inner = 32 | StrippedString::new(inner).map_err(DescriptionError::from_not_stripped_error)?; 33 | Ok(ProjectDescription(inner)) 34 | } 35 | 36 | pub fn as_str(&self) -> &str { 37 | self.0.as_str() 38 | } 39 | 40 | pub fn into_string(self) -> String { 41 | self.0.into_inner().into_inner() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sos21-domain/src/model/project/index.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use crate::model::bound::{Bounded, Unbounded}; 4 | use crate::model::integer::{self, LimitedInteger}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 10 | #[serde(transparent)] 11 | pub struct ProjectIndex(LimitedInteger, u16>); 12 | 13 | #[derive(Debug, Error, Clone)] 14 | #[error("invalid project index")] 15 | pub struct FromU16Error { 16 | _priv: (), 17 | } 18 | 19 | impl FromU16Error { 20 | fn from_integer_error(_err: integer::BoundError) -> Self { 21 | FromU16Error { _priv: () } 22 | } 23 | } 24 | 25 | impl ProjectIndex { 26 | pub fn from_u16(index: u16) -> Result { 27 | let index = LimitedInteger::new(index).map_err(FromU16Error::from_integer_error)?; 28 | Ok(ProjectIndex(index)) 29 | } 30 | 31 | pub fn to_u16(&self) -> u16 { 32 | self.0.into_inner() 33 | } 34 | 35 | pub fn to_i16(&self) -> i16 { 36 | // Since the index is 0 ..= 999, we can safely convert this to i16 37 | self.0.into_inner().try_into().unwrap() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sos21-domain/src/model/registration_form/description.rs: -------------------------------------------------------------------------------- 1 | use crate::model::bound::{Bounded, Unbounded}; 2 | use crate::model::string::{self, LengthLimitedString, StrippedString}; 3 | 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct RegistrationFormDescription( 8 | StrippedString, String>>, 9 | ); 10 | 11 | #[derive(Debug, Error, Clone)] 12 | #[error("invalid form description")] 13 | pub struct DescriptionError { 14 | _priv: (), 15 | } 16 | 17 | impl DescriptionError { 18 | fn from_length_error(_err: string::LengthError>) -> Self { 19 | DescriptionError { _priv: () } 20 | } 21 | 22 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 23 | DescriptionError { _priv: () } 24 | } 25 | } 26 | 27 | impl RegistrationFormDescription { 28 | pub fn from_string(name: impl Into) -> Result { 29 | let inner = 30 | LengthLimitedString::new(name.into()).map_err(DescriptionError::from_length_error)?; 31 | let inner = 32 | StrippedString::new(inner).map_err(DescriptionError::from_not_stripped_error)?; 33 | Ok(RegistrationFormDescription(inner)) 34 | } 35 | 36 | pub fn into_string(self) -> String { 37 | self.0.into_inner().into_inner() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sos21-domain/src/model/registration_form/name.rs: -------------------------------------------------------------------------------- 1 | use crate::model::string::{self, LengthBoundedString, StrippedString}; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct RegistrationFormName( 7 | StrippedString>, 8 | ); 9 | 10 | #[derive(Debug, Error, Clone)] 11 | #[error("invalid registration form name")] 12 | pub struct NameError { 13 | _priv: (), 14 | } 15 | 16 | impl NameError { 17 | fn from_length_error(_err: string::BoundedLengthError) -> Self { 18 | NameError { _priv: () } 19 | } 20 | 21 | fn from_not_stripped_error(_err: string::NotStrippedError) -> Self { 22 | NameError { _priv: () } 23 | } 24 | } 25 | 26 | impl RegistrationFormName { 27 | pub fn from_string(name: impl Into) -> Result { 28 | let inner = LengthBoundedString::new(name.into()).map_err(NameError::from_length_error)?; 29 | let inner = StrippedString::new(inner).map_err(NameError::from_not_stripped_error)?; 30 | Ok(RegistrationFormName(inner)) 31 | } 32 | 33 | pub fn as_str(&self) -> &str { 34 | self.0.as_str() 35 | } 36 | 37 | pub fn into_string(self) -> String { 38 | self.0.into_inner().into_inner() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sos21-domain/src/model/registration_form_answer/respondent.rs: -------------------------------------------------------------------------------- 1 | use crate::model::pending_project::{PendingProject, PendingProjectId}; 2 | use crate::model::project::{Project, ProjectId}; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 5 | pub enum RegistrationFormAnswerRespondent { 6 | Project(ProjectId), 7 | PendingProject(PendingProjectId), 8 | } 9 | 10 | impl RegistrationFormAnswerRespondent { 11 | pub fn replace_to_project(&mut self, project: &Project) -> Self { 12 | std::mem::replace( 13 | self, 14 | RegistrationFormAnswerRespondent::Project(project.id()), 15 | ) 16 | } 17 | 18 | pub fn is_project(&self, project: &Project) -> bool { 19 | matches!(self, 20 | RegistrationFormAnswerRespondent::Project(id) 21 | if project.id() == *id 22 | ) 23 | } 24 | 25 | pub fn is_pending_project(&self, pending_project: &PendingProject) -> bool { 26 | matches!(self, 27 | RegistrationFormAnswerRespondent::PendingProject(id) 28 | if pending_project.id() == *id 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sos21-domain/src/model/user/assignment.rs: -------------------------------------------------------------------------------- 1 | use crate::model::pending_project::PendingProjectId; 2 | use crate::model::project::ProjectId; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 5 | pub enum UserAssignment { 6 | ProjectOwner(ProjectId), 7 | ProjectSubowner(ProjectId), 8 | PendingProjectOwner(PendingProjectId), 9 | } 10 | -------------------------------------------------------------------------------- /sos21-domain/src/model/user/category.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Eq)] 2 | pub enum UserCategory { 3 | UndergraduateStudent, 4 | GraduateStudent, 5 | AcademicStaff, 6 | } 7 | -------------------------------------------------------------------------------- /sos21-domain/src/model/user/file_usage.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub struct UserFileUsage(u64); 3 | 4 | impl UserFileUsage { 5 | pub fn from_number_of_bytes(usage: u64) -> Self { 6 | UserFileUsage(usage) 7 | } 8 | 9 | pub fn to_number_of_bytes(&self) -> u64 { 10 | self.0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sos21-domain/src/model/user_invitation/role.rs: -------------------------------------------------------------------------------- 1 | use crate::model::user::UserRole; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum UserInvitationRole { 5 | Committee, 6 | CommitteeOperator, 7 | Administrator, 8 | } 9 | 10 | impl UserInvitationRole { 11 | pub fn to_user_role(&self) -> UserRole { 12 | match self { 13 | UserInvitationRole::Committee => UserRole::Committee, 14 | UserInvitationRole::CommitteeOperator => UserRole::CommitteeOperator, 15 | UserInvitationRole::Administrator => UserRole::Administrator, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sos21-domain/src/test.rs: -------------------------------------------------------------------------------- 1 | pub mod context; 2 | pub mod model; 3 | 4 | use context::MockAppBuilder; 5 | 6 | pub fn build_mock_app() -> MockAppBuilder { 7 | MockAppBuilder::new() 8 | } 9 | -------------------------------------------------------------------------------- /sos21-domain/src/test/model.rs: -------------------------------------------------------------------------------- 1 | mod user; 2 | pub use user::*; 3 | mod project; 4 | pub use project::*; 5 | mod project_query; 6 | pub use project_query::*; 7 | mod form; 8 | pub use form::*; 9 | mod file; 10 | pub use file::*; 11 | mod file_sharing; 12 | pub use file_sharing::*; 13 | mod file_distribution; 14 | pub use file_distribution::*; 15 | mod object; 16 | pub use object::*; 17 | mod form_answer; 18 | pub use form_answer::*; 19 | mod pending_project; 20 | pub use pending_project::*; 21 | mod registration_form; 22 | pub use registration_form::*; 23 | mod registration_form_answer; 24 | pub use registration_form_answer::*; 25 | mod user_invitation; 26 | pub use user_invitation::*; 27 | mod project_creation_period; 28 | pub use project_creation_period::*; 29 | -------------------------------------------------------------------------------- /sos21-domain/src/test/model/file.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{ 2 | date_time::DateTime, 3 | file::{File, FileBlake3Digest, FileId, FileSize, FileType}, 4 | object::{Object, ObjectId}, 5 | user::UserId, 6 | }; 7 | use crate::test::model as test_model; 8 | use uuid::Uuid; 9 | 10 | pub fn new_file_id() -> FileId { 11 | FileId::from_uuid(Uuid::new_v4()) 12 | } 13 | 14 | pub fn mock_file_type() -> FileType { 15 | FileType::from_mime(mime::IMAGE_PNG) 16 | } 17 | 18 | pub fn new_file_with_object( 19 | author_id: UserId, 20 | object_id: ObjectId, 21 | object_blake3: [u8; 32], 22 | object_size: u64, 23 | ) -> File { 24 | File { 25 | id: new_file_id(), 26 | created_at: DateTime::now(), 27 | author_id, 28 | object_id, 29 | blake3_digest: FileBlake3Digest::from_array(object_blake3), 30 | name: None, 31 | type_: mock_file_type(), 32 | size: FileSize::from_number_of_bytes(object_size), 33 | } 34 | } 35 | 36 | pub fn new_file(author_id: UserId) -> (File, Object) { 37 | let (object, digest, size) = test_model::new_object(); 38 | ( 39 | new_file_with_object(author_id, object.id, digest, size), 40 | object, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /sos21-domain/src/test/model/file_sharing.rs: -------------------------------------------------------------------------------- 1 | use crate::model::date_time::DateTime; 2 | use crate::model::file::FileId; 3 | use crate::model::file_sharing::{ 4 | FileSharing, FileSharingContent, FileSharingId, FileSharingScope, 5 | }; 6 | 7 | use uuid::Uuid; 8 | 9 | pub fn new_file_sharing_id() -> FileSharingId { 10 | FileSharingId::from_uuid(Uuid::new_v4()) 11 | } 12 | 13 | pub fn new_expired_file_sharing(file_id: FileId, scope: FileSharingScope) -> FileSharing { 14 | let expires_at = DateTime::now(); 15 | let created_at = DateTime::from_utc(expires_at.utc() - chrono::Duration::seconds(1)); 16 | 17 | let sharing = FileSharingContent { 18 | id: new_file_sharing_id(), 19 | created_at, 20 | file_id, 21 | is_revoked: false, 22 | expires_at: Some(expires_at), 23 | scope, 24 | }; 25 | FileSharing::from_content(sharing) 26 | } 27 | -------------------------------------------------------------------------------- /sos21-domain/src/test/model/project_creation_period.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{date_time::DateTime, project_creation_period::ProjectCreationPeriod}; 2 | 3 | pub fn mock_project_creation_period_with_start(starts_at: DateTime) -> ProjectCreationPeriod { 4 | let ends_at = DateTime::from_utc(starts_at.utc() + chrono::Duration::hours(1)); 5 | ProjectCreationPeriod::from_datetime(starts_at, ends_at).unwrap() 6 | } 7 | 8 | pub fn mock_project_creation_period_with_end(ends_at: DateTime) -> ProjectCreationPeriod { 9 | let starts_at = DateTime::from_utc(ends_at.utc() - chrono::Duration::hours(1)); 10 | ProjectCreationPeriod::from_datetime(starts_at, ends_at).unwrap() 11 | } 12 | 13 | pub fn new_project_creation_period_from_now() -> ProjectCreationPeriod { 14 | mock_project_creation_period_with_start(DateTime::now()) 15 | } 16 | 17 | pub fn new_project_creation_period_to_now() -> ProjectCreationPeriod { 18 | mock_project_creation_period_with_end(DateTime::now()) 19 | } 20 | 21 | pub fn new_project_creation_period_with_hours_from_now(hours: i64) -> ProjectCreationPeriod { 22 | mock_project_creation_period_with_start(DateTime::from_utc( 23 | chrono::Utc::now() + chrono::Duration::hours(hours), 24 | )) 25 | } 26 | -------------------------------------------------------------------------------- /sos21-domain/src/test/model/project_query.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{ 2 | project::ProjectAttributes, 3 | project_query::{ProjectQuery, ProjectQueryConjunction}, 4 | }; 5 | 6 | pub fn mock_project_query() -> ProjectQuery { 7 | ProjectQuery::from_conjunctions(vec![ProjectQueryConjunction { 8 | category: None, 9 | attributes: ProjectAttributes::from_attributes(Vec::new()).unwrap(), 10 | }]) 11 | .unwrap() 12 | } 13 | -------------------------------------------------------------------------------- /sos21-domain/src/test/model/user_invitation.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{ 2 | date_time::DateTime, 3 | user::{UserEmailAddress, UserId}, 4 | user_invitation::{ 5 | UserInvitation, UserInvitationContent, UserInvitationId, UserInvitationRole, 6 | }, 7 | }; 8 | 9 | use uuid::Uuid; 10 | 11 | pub fn new_user_invitation_id() -> UserInvitationId { 12 | UserInvitationId::from_uuid(Uuid::new_v4()) 13 | } 14 | 15 | pub fn new_user_invitation( 16 | author_id: UserId, 17 | email: S, 18 | role: UserInvitationRole, 19 | ) -> UserInvitation 20 | where 21 | S: Into, 22 | { 23 | UserInvitation::from_content(UserInvitationContent { 24 | id: new_user_invitation_id(), 25 | created_at: DateTime::now(), 26 | author_id, 27 | email: UserEmailAddress::from_string(email).unwrap(), 28 | role, 29 | }) 30 | } 31 | 32 | pub fn new_committee_user_invitation(author_id: UserId, email: S) -> UserInvitation 33 | where 34 | S: Into, 35 | { 36 | new_user_invitation(author_id, email, UserInvitationRole::Committee) 37 | } 38 | 39 | pub fn new_operator_user_invitation(author_id: UserId, email: S) -> UserInvitation 40 | where 41 | S: Into, 42 | { 43 | new_user_invitation(author_id, email, UserInvitationRole::CommitteeOperator) 44 | } 45 | 46 | pub fn new_admin_user_invitation(author_id: UserId, email: S) -> UserInvitation 47 | where 48 | S: Into, 49 | { 50 | new_user_invitation(author_id, email, UserInvitationRole::Administrator) 51 | } 52 | -------------------------------------------------------------------------------- /sos21-gateway/database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-gateway-database" 3 | version = "0.7.1" 4 | authors = ["coord_e ", "azarashi2931 ", "yuseiito ", "momeemt "] 5 | edition = "2018" 6 | readme = "README.md" 7 | documentation = "https://sohosai.github.io/sos21-backend/sos21_gateway_database/" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | futures = "0.3" 13 | async-trait = "0.1.42" 14 | ref-cast = "1" 15 | serde_json = "1" 16 | uuid = "0.8" 17 | sos21-database = { path = "../../sos21-database" } 18 | sos21-domain = { path = "../../sos21-domain" } 19 | sqlx = { version = "0.5", features = ["postgres"] } 20 | 21 | [build-dependencies] 22 | syn = "1" 23 | -------------------------------------------------------------------------------- /sos21-gateway/database/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-gateway-database` 2 | 3 | [![docs (develop)](https://img.shields.io/badge/docs-develop-blue)](https://sohosai.github.io/sos21-backend/develop/sos21_gateway_database/) 4 | [![docs](https://img.shields.io/github/v/release/sohosai/sos21-backend?label=docs&color=blue)](https://sohosai.github.io/sos21-backend/sos21_gateway_database/) 5 | 6 | `sos21-database` を用いて `sos21-domain` の永続化に関するインターフェースを実装します。 7 | -------------------------------------------------------------------------------- /sos21-gateway/database/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-gateway-database 8 | -------------------------------------------------------------------------------- /sos21-gateway/s3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-gateway-s3" 3 | version = "0.7.1" 4 | authors = ["coord.e "] 5 | edition = "2018" 6 | readme = "README.md" 7 | documentation = "https://sohosai.github.io/sos21-backend/sos21_gateway_s3/" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | async-trait = "0.1.42" 13 | bytes = "1" 14 | futures = "0.3" 15 | rusoto_core = { version = "0.47", default-features = false, features = ["rustls"] } 16 | rusoto_s3 = { version = "0.47", default-features = false, features = ["rustls"] } 17 | thiserror = "1" 18 | tokio = { version = "1", default-features = false, features = ["rt"] } 19 | sos21-domain = { path = "../../sos21-domain" } 20 | 21 | [build-dependencies] 22 | syn = "1" -------------------------------------------------------------------------------- /sos21-gateway/s3/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-gateway-s3` 2 | 3 | [![docs (develop)](https://img.shields.io/badge/docs-develop-blue)](https://sohosai.github.io/sos21-backend/develop/sos21_gateway_s3/) 4 | [![docs](https://img.shields.io/github/v/release/sohosai/sos21-backend?label=docs&color=blue)](https://sohosai.github.io/sos21-backend/sos21_gateway_s3/) 5 | 6 | [`rusoto_s3`](https://docs.rs/rusoto_s3/) を用いて `sos21-domain` の永続化に関するインターフェースを実装します。 7 | -------------------------------------------------------------------------------- /sos21-gateway/s3/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-gateway-s3 8 | -------------------------------------------------------------------------------- /sos21-gateway/s3/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Debug}; 2 | 3 | use rusoto_s3::S3Client; 4 | 5 | mod object_repository; 6 | use object_repository::ObjectS3; 7 | 8 | #[derive(Clone)] 9 | pub struct S3 { 10 | object_bucket: String, 11 | client: S3Client, 12 | } 13 | 14 | impl Debug for S3 { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | // `S3Client` does't implement `Debug`, 17 | // so using an unit struct below as a replacement. 18 | #[derive(Debug)] 19 | struct S3Client; 20 | 21 | f.debug_struct("S3") 22 | .field("object_bucket", &self.object_bucket) 23 | .field("client", &S3Client) 24 | .finish() 25 | } 26 | } 27 | 28 | impl S3 { 29 | pub fn new(client: S3Client, object_bucket: impl Into) -> Self { 30 | S3 { 31 | object_bucket: object_bucket.into(), 32 | client, 33 | } 34 | } 35 | } 36 | 37 | sos21_domain::delegate_object_repository! { 38 | impl ObjectRepository for S3 { 39 | Self { ObjectS3 }, 40 | // TODO: Reduce clone() which is too much for the temporary 41 | self { ObjectS3 { bucket: self.object_bucket.clone(), client: self.client.clone() } } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sos21-gateway/slack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-gateway-slack" 3 | version = "0.7.1" 4 | authors = ["yuseiito "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | url = "2.2.2" 12 | slack-hook = "0.8.0" 13 | sos21-domain = { path = "../../sos21-domain" } 14 | 15 | [build-dependencies] 16 | syn = "1" 17 | -------------------------------------------------------------------------------- /sos21-gateway/slack/src/lib.rs: -------------------------------------------------------------------------------- 1 | use slack_hook::{PayloadBuilder, Slack, SlackTextContent::Text}; 2 | use sos21_domain::model::{form::FormName, project::ProjectName}; 3 | 4 | pub fn send_form_answer_notification( 5 | hook: &str, 6 | project_name: &ProjectName, 7 | form_name: &FormName, 8 | ) -> Result<(), slack_hook::Error> { 9 | let slack = Slack::new(hook)?; 10 | let payload = PayloadBuilder::new() 11 | .text( 12 | vec![Text( 13 | format!( 14 | "企画「{}」が申請「{}」に回答しました。", 15 | project_name.as_str(), 16 | form_name.as_str() 17 | ) 18 | .into(), 19 | )] 20 | .as_slice(), 21 | ) 22 | .build()?; 23 | 24 | slack.send(&payload) 25 | } 26 | 27 | pub fn report_suspicious_email(hook: &str, email: &str) -> Result<(), slack_hook::Error> { 28 | let slack = Slack::new(hook)?; 29 | let payload = PayloadBuilder::new() 30 | .text( 31 | vec![Text( 32 | format!( 33 | "不審なメールアドレス(有効なJWTを所持)からのアクセスを検知。 アカウントのメールアドレス: {}.", 34 | email 35 | ) 36 | .into(), 37 | )] 38 | .as_slice(), 39 | ) 40 | .build()?; 41 | 42 | slack.send(&payload) 43 | } 44 | -------------------------------------------------------------------------------- /sos21-run-migrations/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-run-migrations" 3 | version = "0.7.1" 4 | authors = ["coord_e ", "azarashi2931 ", "yuseiito ", "momeemt "] 5 | edition = "2018" 6 | readme = "README.md" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | sos21-database = { path = "../sos21-database" } 12 | sqlx = { version = "0.5", features = ["postgres", "runtime-tokio-rustls"] } 13 | structopt = "0.3" 14 | tokio = { version = "1", features = ["full"] } 15 | tracing = "0.1" 16 | tracing-futures = "0.2" 17 | tracing-subscriber = "0.2" 18 | 19 | [build-dependencies] 20 | syn = "1" 21 | -------------------------------------------------------------------------------- /sos21-run-migrations/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-run-migrations` 2 | 3 | マイグレーションを実行する CLI アプリケーションです。 4 | 5 | SQL の検証のためにコンパイル時にマイグレーションが適用されている必要があるため、 6 | 開発時にはこのアプリケーションは使用しません。 7 | マイグレーションの方法については [`sos21-database`](../sos21-database/README.md) を参照してください。 8 | -------------------------------------------------------------------------------- /sos21-run-migrations/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-run-migrations 8 | -------------------------------------------------------------------------------- /sos21-use-case/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sos21-use-case" 3 | version = "0.7.1" 4 | authors = ["coord_e ", "azarashi2931 ", "yuseiito ", "momeemt "] 5 | edition = "2018" 6 | readme = "README.md" 7 | documentation = "https://sohosai.github.io/sos21-backend/sos21_use_case/" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | bytes = "1" 13 | chrono = "0.4" 14 | csv = "1" 15 | futures = "0.3" 16 | mime = "0.3" 17 | tracing = "0.1" 18 | tracing-futures = "0.2" 19 | uuid = { version = "0.8", features = ["v4"] } 20 | sos21-domain = { path = "../sos21-domain" } 21 | sos21-gateway-slack = { path = "../sos21-gateway/slack" } 22 | 23 | [dev-dependencies] 24 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 25 | sos21-domain = { path = "../sos21-domain", features = ["test"] } 26 | 27 | [build-dependencies] 28 | syn = "1" 29 | -------------------------------------------------------------------------------- /sos21-use-case/README.md: -------------------------------------------------------------------------------- 1 | # `sos21-use-case` 2 | 3 | [![docs (develop)](https://img.shields.io/badge/docs-develop-blue)](https://sohosai.github.io/sos21-backend/develop/sos21_use_case/) 4 | [![docs](https://img.shields.io/github/v/release/sohosai/sos21-backend?label=docs&color=blue)](https://sohosai.github.io/sos21-backend/sos21_use_case/) 5 | 6 | ユースケース層。アプリケーション固有のビジネスロジックを記述します。 7 | -------------------------------------------------------------------------------- /sos21-use-case/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../nix/pkgs.nix 2 | , runTests ? true 3 | }: 4 | let 5 | sos21-backend = import ../. { inherit pkgs runTests; }; 6 | in 7 | sos21-backend.sos21-use-case 8 | -------------------------------------------------------------------------------- /sos21-use-case/src/error.rs: -------------------------------------------------------------------------------- 1 | use sos21_domain::DomainError; 2 | 3 | #[derive(Debug)] 4 | pub enum UseCaseError { 5 | UseCase(E), 6 | Internal(anyhow::Error), 7 | } 8 | 9 | impl UseCaseError { 10 | pub fn map_use_case(self, op: F) -> UseCaseError 11 | where 12 | F: FnOnce(T) -> U, 13 | { 14 | match self { 15 | UseCaseError::UseCase(err) => UseCaseError::UseCase(op(err)), 16 | UseCaseError::Internal(err) => UseCaseError::Internal(err), 17 | } 18 | } 19 | 20 | pub fn from_domain(err: DomainError, op: F) -> Self 21 | where 22 | F: FnOnce(U) -> T, 23 | { 24 | match err { 25 | DomainError::Domain(err) => UseCaseError::UseCase(op(err)), 26 | DomainError::Internal(err) => UseCaseError::Internal(err), 27 | } 28 | } 29 | } 30 | 31 | impl UseCaseError> { 32 | pub fn flatten(self) -> UseCaseError { 33 | match self { 34 | UseCaseError::UseCase(err) => err, 35 | UseCaseError::Internal(err) => UseCaseError::Internal(err), 36 | } 37 | } 38 | } 39 | 40 | impl From for UseCaseError { 41 | fn from(e: anyhow::Error) -> Self { 42 | UseCaseError::Internal(e) 43 | } 44 | } 45 | 46 | pub type UseCaseResult = Result>; 47 | -------------------------------------------------------------------------------- /sos21-use-case/src/get_login_user.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use crate::error::UseCaseResult; 4 | use crate::model::user::User; 5 | 6 | use sos21_domain::context::Login; 7 | 8 | #[tracing::instrument(skip(ctx))] 9 | pub async fn run(ctx: &Login) -> UseCaseResult { 10 | Ok(User::from_entity(ctx.login_user().clone())) 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use crate::get_login_user; 16 | use crate::model::user::{UserId, UserName}; 17 | use sos21_domain::test; 18 | 19 | #[tokio::test] 20 | async fn test_get() { 21 | let user = test::model::new_general_user(); 22 | let app = test::build_mock_app() 23 | .users(vec![user.clone()]) 24 | .build() 25 | .login_as(user.clone()) 26 | .await; 27 | 28 | assert!(matches!( 29 | get_login_user::run(&app).await, 30 | Ok(got) 31 | if got.id == UserId::from_entity(user.id().clone()) 32 | && got.name == UserName::from_entity(user.name().clone()) 33 | )); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sos21-use-case/src/interface.rs: -------------------------------------------------------------------------------- 1 | pub mod form; 2 | pub mod form_answer; 3 | pub mod project_query; 4 | -------------------------------------------------------------------------------- /sos21-use-case/src/interface/form.rs: -------------------------------------------------------------------------------- 1 | mod condition; 2 | pub use condition::{to_form_condition, FormConditionError}; 3 | 4 | mod item; 5 | pub use item::{to_form_item, to_form_items, FormItemError, FormItemsError}; 6 | 7 | mod check_answer_error; 8 | pub use check_answer_error::{ 9 | to_check_answer_error, to_check_answer_item_error, CheckAnswerError, CheckAnswerItemError, 10 | }; 11 | -------------------------------------------------------------------------------- /sos21-use-case/src/interface/form_answer.rs: -------------------------------------------------------------------------------- 1 | mod item; 2 | pub use item::{ 3 | to_form_answer_items, to_registration_form_answer_items, 4 | to_registration_form_answer_items_with_project, FormAnswerItemError, FormAnswerItemsError, 5 | InputFormAnswerItem, InputFormAnswerItemBody, InputFormAnswerItemFile, 6 | }; 7 | -------------------------------------------------------------------------------- /sos21-use-case/src/model.rs: -------------------------------------------------------------------------------- 1 | //! Data transfer object in the use case layer. 2 | 3 | pub mod file; 4 | pub mod file_distribution; 5 | pub mod file_sharing; 6 | pub mod form; 7 | pub mod form_answer; 8 | pub mod pending_project; 9 | pub mod project; 10 | pub mod project_creation_availability; 11 | pub mod project_query; 12 | pub mod registration_form; 13 | pub mod registration_form_answer; 14 | pub mod stream; 15 | pub mod user; 16 | pub mod user_invitation; 17 | -------------------------------------------------------------------------------- /sos21-use-case/src/model/form/item/checkbox.rs: -------------------------------------------------------------------------------- 1 | use sos21_domain::model::form::item::checkbox as entity; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 5 | pub struct CheckboxId(pub Uuid); 6 | 7 | impl CheckboxId { 8 | pub fn from_entity(id: entity::CheckboxId) -> CheckboxId { 9 | CheckboxId(id.to_uuid()) 10 | } 11 | 12 | pub fn into_entity(self) -> entity::CheckboxId { 13 | entity::CheckboxId::from_uuid(self.0) 14 | } 15 | } 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct Checkbox { 19 | pub id: CheckboxId, 20 | pub label: String, 21 | } 22 | 23 | impl Checkbox { 24 | pub fn from_entity(checkbox: entity::Checkbox) -> Self { 25 | Checkbox { 26 | id: CheckboxId::from_entity(checkbox.id), 27 | label: checkbox.label.into_string(), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sos21-use-case/src/model/form/item/radio.rs: -------------------------------------------------------------------------------- 1 | use sos21_domain::model::form::item::radio as entity; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 5 | pub struct RadioId(pub Uuid); 6 | 7 | impl RadioId { 8 | pub fn from_entity(id: entity::RadioId) -> RadioId { 9 | RadioId(id.to_uuid()) 10 | } 11 | 12 | pub fn into_entity(self) -> entity::RadioId { 13 | entity::RadioId::from_uuid(self.0) 14 | } 15 | } 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct Radio { 19 | pub id: RadioId, 20 | pub label: String, 21 | } 22 | 23 | impl Radio { 24 | pub fn from_entity(radio: entity::Radio) -> Self { 25 | Radio { 26 | id: RadioId::from_entity(radio.id), 27 | label: radio.label.into_string(), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sos21-use-case/src/model/form_answer.rs: -------------------------------------------------------------------------------- 1 | use crate::model::form::FormId; 2 | use crate::model::project::ProjectId; 3 | use crate::model::user::UserId; 4 | 5 | use chrono::{DateTime, Utc}; 6 | use sos21_domain::model::form_answer as entity; 7 | use uuid::Uuid; 8 | 9 | pub mod item; 10 | pub use item::FormAnswerItem; 11 | 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 13 | pub struct FormAnswerId(pub Uuid); 14 | 15 | impl FormAnswerId { 16 | pub fn from_entity(id: entity::FormAnswerId) -> FormAnswerId { 17 | FormAnswerId(id.to_uuid()) 18 | } 19 | 20 | pub fn into_entity(self) -> entity::FormAnswerId { 21 | entity::FormAnswerId::from_uuid(self.0) 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct FormAnswer { 27 | pub id: FormAnswerId, 28 | pub project_id: ProjectId, 29 | pub form_id: FormId, 30 | pub created_at: DateTime, 31 | pub author_id: UserId, 32 | pub items: Vec, 33 | } 34 | 35 | impl FormAnswer { 36 | pub fn from_entity(answer: entity::FormAnswer) -> Self { 37 | FormAnswer { 38 | id: FormAnswerId::from_entity(answer.id()), 39 | project_id: ProjectId::from_entity(answer.project_id()), 40 | form_id: FormId::from_entity(answer.form_id()), 41 | created_at: answer.created_at().utc(), 42 | author_id: UserId::from_entity(answer.author_id().clone()), 43 | items: answer 44 | .into_items() 45 | .into_items() 46 | .map(FormAnswerItem::from_entity) 47 | .collect(), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sos21-use-case/src/model/project_creation_availability.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub struct ProjectCreationAvailability { 5 | pub timestamp: DateTime, 6 | pub general: bool, 7 | pub cooking_requiring_preparation_area: bool, 8 | pub cooking: bool, 9 | pub food: bool, 10 | pub stage: bool, 11 | } 12 | -------------------------------------------------------------------------------- /sos21-use-case/src/model/project_query.rs: -------------------------------------------------------------------------------- 1 | use crate::model::project::{ProjectAttribute, ProjectCategory}; 2 | 3 | use sos21_domain::model::project_query as entity; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct ProjectQueryConjunction { 7 | pub category: Option, 8 | pub attributes: Vec, 9 | } 10 | 11 | impl ProjectQueryConjunction { 12 | pub fn from_entity(conj: entity::ProjectQueryConjunction) -> Self { 13 | ProjectQueryConjunction { 14 | category: conj.category().map(ProjectCategory::from_entity), 15 | attributes: conj 16 | .attributes() 17 | .map(ProjectAttribute::from_entity) 18 | .collect(), 19 | } 20 | } 21 | } 22 | 23 | #[derive(Debug, Clone, PartialEq, Eq)] 24 | pub struct ProjectQuery(pub Vec); 25 | 26 | impl ProjectQuery { 27 | pub fn from_entity(query: entity::ProjectQuery) -> Self { 28 | ProjectQuery( 29 | query 30 | .into_conjunctions() 31 | .map(ProjectQueryConjunction::from_entity) 32 | .collect(), 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sos21-use-case/src/model/stream.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Debug}; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use bytes::Bytes; 6 | use futures::stream::{Stream, StreamExt, TryStreamExt}; 7 | 8 | pub struct ByteStream(Pin> + Send + 'static>>); 9 | 10 | impl Debug for ByteStream { 11 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 12 | f.debug_tuple("ByteStream").finish() 13 | } 14 | } 15 | 16 | impl Stream for ByteStream { 17 | type Item = anyhow::Result; 18 | 19 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 20 | self.0.poll_next_unpin(cx) 21 | } 22 | } 23 | 24 | impl ByteStream { 25 | pub fn new(stream: S) -> Self 26 | where 27 | S: Stream> + Send + 'static, 28 | E: Into + Send + 'static, 29 | { 30 | ByteStream(Box::pin(stream.map_err(Into::into))) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sos21-use-case/src/test.rs: -------------------------------------------------------------------------------- 1 | pub mod interface; 2 | -------------------------------------------------------------------------------- /sos21-use-case/src/test/interface.rs: -------------------------------------------------------------------------------- 1 | mod form_answer; 2 | pub use form_answer::*; 3 | --------------------------------------------------------------------------------