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 %}
+
+{% 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 %}
+
+
+
+
+
+
+ |
+ {{"user_id" | capitalize }}
+ |
+
+ {{"title" | capitalize }}
+ |
+
+ {{"content" | capitalize }}
+ |
+
+ Actions
+ |
+
+
+
+ {% for item in items %}
+
+ |
+ {{item.user_id | escape }}
+ |
+
+ {{item.title | escape }}
+ |
+
+ {{item.content | escape }}
+ |
+
+ Edit
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+ {% 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);
+}