From d640ce35b6140fa8b73b0754c5d87817e2504435 Mon Sep 17 00:00:00 2001 From: Fedor Lyanguzov Date: Sun, 26 Jul 2026 14:45:55 +0300 Subject: [PATCH] Add note scaffold --- notes-service/assets/views/base.html | 108 +++++++++++++++++ notes-service/assets/views/note/create.html | 37 ++++++ notes-service/assets/views/note/edit.html | 43 +++++++ notes-service/assets/views/note/list.html | 86 +++++++++++++ notes-service/assets/views/note/show.html | 26 ++++ notes-service/migration/src/lib.rs | 4 +- .../migration/src/m20260726_113842_notes.rs | 27 +++++ notes-service/src/app.rs | 3 +- notes-service/src/controllers/mod.rs | 2 + notes-service/src/controllers/note.rs | 113 ++++++++++++++++++ notes-service/src/models/_entities/mod.rs | 4 +- notes-service/src/models/_entities/notes.rs | 35 ++++++ notes-service/src/models/_entities/prelude.rs | 4 +- notes-service/src/models/_entities/users.rs | 17 ++- notes-service/src/models/mod.rs | 1 + notes-service/src/models/notes.rs | 28 +++++ notes-service/src/views/mod.rs | 2 + notes-service/src/views/note.rs | 39 ++++++ notes-service/tests/models/mod.rs | 2 + notes-service/tests/models/notes.rs | 31 +++++ 20 files changed, 604 insertions(+), 8 deletions(-) create mode 100644 notes-service/assets/views/base.html create mode 100644 notes-service/assets/views/note/create.html create mode 100644 notes-service/assets/views/note/edit.html create mode 100644 notes-service/assets/views/note/list.html create mode 100644 notes-service/assets/views/note/show.html create mode 100644 notes-service/migration/src/m20260726_113842_notes.rs create mode 100644 notes-service/src/controllers/note.rs create mode 100644 notes-service/src/models/_entities/notes.rs create mode 100644 notes-service/src/models/notes.rs create mode 100644 notes-service/src/views/note.rs create mode 100644 notes-service/tests/models/notes.rs diff --git a/notes-service/assets/views/base.html b/notes-service/assets/views/base.html new file mode 100644 index 00000000..8c7401f5 --- /dev/null +++ b/notes-service/assets/views/base.html @@ -0,0 +1,108 @@ + + + + + + + + {% block title %}{% endblock title %} + + + + {% block head %} + + {% endblock head %} + + + +
+
+
+
+

+ {% block page_title %}{% endblock page_title %} +

+ {% block content %} + {% endblock content %} +
+
+
+
+ {% block js %} + + {% endblock js %} + + + + + \ No newline at end of file diff --git a/notes-service/assets/views/note/create.html b/notes-service/assets/views/note/create.html new file mode 100644 index 00000000..51ae64f2 --- /dev/null +++ b/notes-service/assets/views/note/create.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %} +Create note +{% endblock title %} + +{% block page_title %} +Create new note +{% endblock page_title %} + +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+{% endblock content %} + +{% block js %} + +{% endblock js %} \ No newline at end of file diff --git a/notes-service/assets/views/note/edit.html b/notes-service/assets/views/note/edit.html new file mode 100644 index 00000000..df70ae9d --- /dev/null +++ b/notes-service/assets/views/note/edit.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %} +Edit note: {{ item.id }} +{% endblock title %} + +{% block page_title %} +Edit note: {{ item.id }} +{% endblock page_title %} + +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ Back to note +
+{% endblock content %} + +{% block js %} + +{% endblock js %} \ No newline at end of file diff --git a/notes-service/assets/views/note/list.html b/notes-service/assets/views/note/list.html new file mode 100644 index 00000000..5135beb1 --- /dev/null +++ b/notes-service/assets/views/note/list.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} + +{% block title %} +List of note +{% endblock title %} + +{% block page_title %} +note +{% endblock page_title %} + +{% block content %} +
+ + {% if items %} + +
+
+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
+ {{"user_id" | capitalize }} + + {{"title" | capitalize }} + + {{"content" | capitalize }} + + Actions +
+ {{item.user_id | escape }} + + {{item.title | escape }} + + {{item.content | escape }} + + Edit +
+
+ +
+ +
+
+ + {% else %} + +
+
+

Nothing Here Yet

+ There are no records to display. Add a new record to get started! + + Create + +
+
+ + {% endif %} + + +
+{% endblock content %} diff --git a/notes-service/assets/views/note/show.html b/notes-service/assets/views/note/show.html new file mode 100644 index 00000000..c28b5d9f --- /dev/null +++ b/notes-service/assets/views/note/show.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %} +View note: {{ item.id }} +{% endblock title %} + +{% block page_title %} +View note: {{ item.id }} +{% endblock page_title %} + + +{% block content %} +
+
+ +
+
+ +
+
+ +
+
+Back to notes +
+{% endblock content %} diff --git a/notes-service/migration/src/lib.rs b/notes-service/migration/src/lib.rs index 86c1ae7e..fc044a22 100644 --- a/notes-service/migration/src/lib.rs +++ b/notes-service/migration/src/lib.rs @@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*; mod m20220101_000001_users; +mod m20260726_113842_notes; pub struct Migrator; #[async_trait::async_trait] @@ -10,7 +11,8 @@ impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ Box::new(m20220101_000001_users::Migration), + Box::new(m20260726_113842_notes::Migration), // inject-above (do not remove this comment) ] } -} +} \ No newline at end of file diff --git a/notes-service/migration/src/m20260726_113842_notes.rs b/notes-service/migration/src/m20260726_113842_notes.rs new file mode 100644 index 00000000..d3adda37 --- /dev/null +++ b/notes-service/migration/src/m20260726_113842_notes.rs @@ -0,0 +1,27 @@ +use loco_rs::schema::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> { + create_table(m, "notes", + &[ + + ("id", ColType::PkAuto), + + ("title", ColType::StringNull), + ("content", ColType::TextNull), + ], + &[ + ("user", ""), + ] + ).await + } + + async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> { + drop_table(m, "notes").await + } +} diff --git a/notes-service/src/app.rs b/notes-service/src/app.rs index 29d5f001..7aa1ac0a 100644 --- a/notes-service/src/app.rs +++ b/notes-service/src/app.rs @@ -51,6 +51,7 @@ impl Hooks for App { fn routes(_ctx: &AppContext) -> AppRoutes { AppRoutes::with_default_routes() // controller routes below + .add_route(controllers::note::routes()) .add_route(controllers::auth::routes()) } async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { @@ -71,4 +72,4 @@ impl Hooks for App { .await?; Ok(()) } -} +} \ No newline at end of file diff --git a/notes-service/src/controllers/mod.rs b/notes-service/src/controllers/mod.rs index 0e4a05d5..14e595ba 100644 --- a/notes-service/src/controllers/mod.rs +++ b/notes-service/src/controllers/mod.rs @@ -1 +1,3 @@ pub mod auth; + +pub mod note; \ No newline at end of file diff --git a/notes-service/src/controllers/note.rs b/notes-service/src/controllers/note.rs new file mode 100644 index 00000000..1f85f220 --- /dev/null +++ b/notes-service/src/controllers/note.rs @@ -0,0 +1,113 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use loco_rs::prelude::*; +use sea_orm::{sea_query::Order, QueryOrder}; +use serde::{Deserialize, Serialize}; + +use crate::{ + models::_entities::notes::{ActiveModel, Column, Entity, Model}, + views, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + pub user_id: i64, + pub title: Option, + pub content: Option, +} + +impl Params { + fn update(&self, item: &mut ActiveModel) { + item.user_id = Set(self.user_id); + item.title = Set(self.title.clone()); + item.content = Set(self.content.clone()); + } +} + +async fn load_item(ctx: &AppContext, id: i64) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +pub async fn list( + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let item = Entity::find() + .order_by(Column::Id, Order::Desc) + .all(&ctx.db) + .await?; + views::note::list(&v, &item) +} + +#[debug_handler] +pub async fn new( + ViewEngine(v): ViewEngine, + State(_ctx): State, +) -> Result { + views::note::create(&v) +} + +#[debug_handler] +pub async fn update( + Path(id): Path, + State(ctx): State, + Json(params): Json, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + let _ = item.update(&ctx.db).await?; + format::render().redirect_with_header_key("HX-Redirect", "/notes") +} + +#[debug_handler] +pub async fn edit( + Path(id): Path, + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let item = load_item(&ctx, id).await?; + views::note::edit(&v, &item) +} + +#[debug_handler] +pub async fn show( + Path(id): Path, + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let item = load_item(&ctx, id).await?; + views::note::show(&v, &item) +} + +#[debug_handler] +pub async fn add(State(ctx): State, Json(params): Json) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + let _ = item.insert(&ctx.db).await?; + format::render().redirect_with_header_key("HX-Redirect", "/notes") +} + +#[debug_handler] +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("notes/") + .add("/", get(list)) + .add("/", post(add)) + .add("new", get(new)) + .add("{id}", get(show)) + .add("{id}/edit", get(edit)) + .add("{id}", delete(remove)) + .add("{id}", put(update)) + .add("{id}", patch(update)) +} diff --git a/notes-service/src/models/_entities/mod.rs b/notes-service/src/models/_entities/mod.rs index 7efa3a09..dd183b5a 100644 --- a/notes-service/src/models/_entities/mod.rs +++ b/notes-service/src/models/_entities/mod.rs @@ -1,4 +1,6 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 pub mod prelude; + +pub mod notes; pub mod users; diff --git a/notes-service/src/models/_entities/notes.rs b/notes-service/src/models/_entities/notes.rs new file mode 100644 index 00000000..e5eefeca --- /dev/null +++ b/notes-service/src/models/_entities/notes.rs @@ -0,0 +1,35 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "notes")] +pub struct Model { + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + #[sea_orm(primary_key)] + pub id: i64, + pub title: Option, + #[sea_orm(column_type = "Text", nullable)] + pub content: Option, + pub user_id: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} diff --git a/notes-service/src/models/_entities/prelude.rs b/notes-service/src/models/_entities/prelude.rs index 1055169a..b76a9539 100644 --- a/notes-service/src/models/_entities/prelude.rs +++ b/notes-service/src/models/_entities/prelude.rs @@ -1,2 +1,4 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +pub use super::notes::Entity as Notes; pub use super::users::Entity as Users; diff --git a/notes-service/src/models/_entities/users.rs b/notes-service/src/models/_entities/users.rs index 765e9927..016e1283 100644 --- a/notes-service/src/models/_entities/users.rs +++ b/notes-service/src/models/_entities/users.rs @@ -1,15 +1,15 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "users")] pub struct Model { pub created_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone, #[sea_orm(primary_key)] - pub id: i32, + pub id: i64, pub pid: Uuid, #[sea_orm(unique)] pub email: String, @@ -27,4 +27,13 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::notes::Entity")] + Notes, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Notes.def() + } +} diff --git a/notes-service/src/models/mod.rs b/notes-service/src/models/mod.rs index 48da463b..ee0b7514 100644 --- a/notes-service/src/models/mod.rs +++ b/notes-service/src/models/mod.rs @@ -1,2 +1,3 @@ pub mod _entities; pub mod users; +pub mod notes; diff --git a/notes-service/src/models/notes.rs b/notes-service/src/models/notes.rs new file mode 100644 index 00000000..a372011f --- /dev/null +++ b/notes-service/src/models/notes.rs @@ -0,0 +1,28 @@ +use sea_orm::entity::prelude::*; +pub use super::_entities::notes::{ActiveModel, Model, Entity}; +pub type Notes = Entity; + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> std::result::Result + where + C: ConnectionTrait, + { + if !insert && self.updated_at.is_unchanged() { + let mut this = self; + this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into()); + Ok(this) + } else { + Ok(self) + } + } +} + +// implement your read-oriented logic here +impl Model {} + +// implement your write-oriented logic here +impl ActiveModel {} + +// implement your custom finders, selectors oriented logic here +impl Entity {} diff --git a/notes-service/src/views/mod.rs b/notes-service/src/views/mod.rs index 0e4a05d5..14e595ba 100644 --- a/notes-service/src/views/mod.rs +++ b/notes-service/src/views/mod.rs @@ -1 +1,3 @@ pub mod auth; + +pub mod note; \ No newline at end of file diff --git a/notes-service/src/views/note.rs b/notes-service/src/views/note.rs new file mode 100644 index 00000000..29b588b6 --- /dev/null +++ b/notes-service/src/views/note.rs @@ -0,0 +1,39 @@ +use loco_rs::prelude::*; + +use crate::models::_entities::notes; + +/// Render a list view of `notes`. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn list(v: &impl ViewRenderer, items: &Vec) -> Result { + format::render().view(v, "note/list.html", data!({"items": items})) +} + +/// Render a single `note` view. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn show(v: &impl ViewRenderer, item: ¬es::Model) -> Result { + format::render().view(v, "note/show.html", data!({"item": item})) +} + +/// Render a `note` create form. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn create(v: &impl ViewRenderer) -> Result { + format::render().view(v, "note/create.html", data!({})) +} + +/// Render a `note` edit form. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn edit(v: &impl ViewRenderer, item: ¬es::Model) -> Result { + format::render().view(v, "note/edit.html", data!({"item": item})) +} diff --git a/notes-service/tests/models/mod.rs b/notes-service/tests/models/mod.rs index 59759880..b9fb1b5e 100644 --- a/notes-service/tests/models/mod.rs +++ b/notes-service/tests/models/mod.rs @@ -1 +1,3 @@ mod users; + +mod notes; \ No newline at end of file diff --git a/notes-service/tests/models/notes.rs b/notes-service/tests/models/notes.rs new file mode 100644 index 00000000..7f7deaf2 --- /dev/null +++ b/notes-service/tests/models/notes.rs @@ -0,0 +1,31 @@ +use notes_service::app::App; +use loco_rs::testing::prelude::*; +use serial_test::serial; + +macro_rules! configure_insta { + ($($expr:expr),*) => { + let mut settings = insta::Settings::clone_current(); + settings.set_prepend_module_to_snapshot(false); + let _guard = settings.bind_to_scope(); + }; +} + +#[tokio::test] +#[serial] +async fn test_model() { + configure_insta!(); + + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context).await.unwrap(); + + // query your model, e.g.: + // + // let item = models::posts::Model::find_by_pid( + // &boot.app_context.db, + // "11111111-1111-1111-1111-111111111111", + // ) + // .await; + + // snapshot the result: + // assert_debug_snapshot!(item); +}