From b5e9c4656c856db52be189bbcd955dad30293f17 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 28 May 2026 15:34:34 -0700 Subject: [PATCH] feat: update database-app example Co-Authored-By: Claude Sonnet 4.6 --- .../app/http/controllers/auth_controller.py | 2 + example/database-app/app/http/schemas/auth.py | 2 + example/database-app/app/models/course.py | 15 ++++-- example/database-app/app/models/review.py | 5 +- example/database-app/app/models/user.py | 8 +-- .../app/students/controllers/registration.py | 14 ++--- example/database-app/bootstrap/application.py | 1 + example/database-app/config/app.py | 1 + example/database-app/config/database.py | 34 ++++++------ example/database-app/config/logging.py | 29 ++++++----- .../2026_04_27_145007_create_courses_table.py | 8 ++- ...6_04_27_145008_create_course_user_table.py | 4 +- .../2026_04_27_145008_create_lessons_table.py | 4 +- .../databases/seeds/category_seeder.py | 2 +- .../databases/seeds/course_seeder.py | 4 +- .../databases/seeds/database_seeder.py | 2 +- .../databases/seeds/post_seeder.py | 24 +++++---- .../databases/seeds/review_seeder.py | 8 ++- .../databases/seeds/user_seeder.py | 6 ++- .../providers/console_provider.py | 1 + .../providers/fastapi_provider.py | 1 + .../tests/features/students/test_register.py | 52 ++++++++++++------- .../tests/features/teachers/test_register.py | 9 +++- example/database-app/tests/test_case.py | 3 +- 24 files changed, 146 insertions(+), 93 deletions(-) diff --git a/example/database-app/app/http/controllers/auth_controller.py b/example/database-app/app/http/controllers/auth_controller.py index 221bd3ce..9dd18cef 100644 --- a/example/database-app/app/http/controllers/auth_controller.py +++ b/example/database-app/app/http/controllers/auth_controller.py @@ -5,6 +5,7 @@ from app.models.profile import Profile from app.http.schemas.auth import StudentRegistrationRequest, TeacherRegistrationRequest + class AuthController: @staticmethod async def register_teacher(data: TeacherRegistrationRequest): @@ -37,6 +38,7 @@ async def register_teacher(data: TeacherRegistrationRequest): profile.video_url = data.video_url profile.hourly_rate = data.hourly_rate import json + profile.languages_spoken = json.dumps(data.languages_spoken) profile.subjects = json.dumps(data.subjects) await profile.save() diff --git a/example/database-app/app/http/schemas/auth.py b/example/database-app/app/http/schemas/auth.py index 88cb46e4..06b0e14a 100644 --- a/example/database-app/app/http/schemas/auth.py +++ b/example/database-app/app/http/schemas/auth.py @@ -1,10 +1,12 @@ from pydantic import BaseModel, EmailStr, Field + class StudentRegistrationRequest(BaseModel): name: str = Field(..., min_length=2, max_length=255) email: EmailStr password: str = Field(..., min_length=8) + class TeacherRegistrationRequest(StudentRegistrationRequest): country: str = Field(..., min_length=2) phone_number: str diff --git a/example/database-app/app/models/course.py b/example/database-app/app/models/course.py index 7b31a9a6..4d5ebbc8 100644 --- a/example/database-app/app/models/course.py +++ b/example/database-app/app/models/course.py @@ -1,7 +1,12 @@ from typing import TYPE_CHECKING from fastapi_startkit.masoniteorm.models import Model -from fastapi_startkit.masoniteorm.relationships import BelongsTo, HasMany, BelongsToMany, MorphMany +from fastapi_startkit.masoniteorm.relationships import ( + BelongsTo, + HasMany, + BelongsToMany, + MorphMany, +) if TYPE_CHECKING: from app.models.category import Category @@ -20,11 +25,11 @@ class Course(Model): category = BelongsTo("Category") lessons = HasMany("Lesson") students = BelongsToMany( - "User", - local_foreign_key="course_id", - other_foreign_key="user_id", + "User", + local_foreign_key="course_id", + other_foreign_key="user_id", table="course_user", with_timestamps=True, - with_fields=["progress", "completed_at"] + with_fields=["progress", "completed_at"], ) reviews = MorphMany("Review", "reviewable_type", "reviewable_id") diff --git a/example/database-app/app/models/review.py b/example/database-app/app/models/review.py index f36f2738..bf6daaff 100644 --- a/example/database-app/app/models/review.py +++ b/example/database-app/app/models/review.py @@ -3,10 +3,13 @@ from fastapi_startkit.masoniteorm.models import Model from fastapi_startkit.masoniteorm.relationships import MorphTo + class Review(Model): __table__ = "reviews" reviewable_type: str content: str - reviewable = MorphTo("Review", morph_key="reviewable_type", morph_id="reviewable_id") + reviewable = MorphTo( + "Review", morph_key="reviewable_type", morph_id="reviewable_id" + ) diff --git a/example/database-app/app/models/user.py b/example/database-app/app/models/user.py index 81107cee..e6d1143c 100644 --- a/example/database-app/app/models/user.py +++ b/example/database-app/app/models/user.py @@ -17,10 +17,10 @@ class User(Model): profile = HasOne("Profile") courses = BelongsToMany( - "Course", - local_foreign_key="user_id", - other_foreign_key="course_id", + "Course", + local_foreign_key="user_id", + other_foreign_key="course_id", table="course_user", with_timestamps=True, - with_fields=["progress", "completed_at"] + with_fields=["progress", "completed_at"], ) diff --git a/example/database-app/app/students/controllers/registration.py b/example/database-app/app/students/controllers/registration.py index d6b27f84..9a137f4b 100644 --- a/example/database-app/app/students/controllers/registration.py +++ b/example/database-app/app/students/controllers/registration.py @@ -10,12 +10,14 @@ async def register(request: StudentRegistrationRequest): existing_user = await User.where("email", request.email).first() if existing_user: raise RequestValidationError( - errors=[{ - "loc": ("body", "email"), - "msg": "Email already registered", - "type": "value_error", - "input": request.email, - }] + errors=[ + { + "loc": ("body", "email"), + "msg": "Email already registered", + "type": "value_error", + "input": request.email, + } + ] ) password = hashlib.md5(request.password.encode()).hexdigest() diff --git a/example/database-app/bootstrap/application.py b/example/database-app/bootstrap/application.py index c764e406..e359a194 100644 --- a/example/database-app/bootstrap/application.py +++ b/example/database-app/bootstrap/application.py @@ -16,6 +16,7 @@ class AppExceptionHandler(ExceptionHandler): def register(self): pass + app: Application[AppConfig] = Application( base_path=Path(__file__).parent.parent, config=AppConfig, diff --git a/example/database-app/config/app.py b/example/database-app/config/app.py index afcda6f5..53484e58 100644 --- a/example/database-app/config/app.py +++ b/example/database-app/config/app.py @@ -4,6 +4,7 @@ from config.database import DatabaseConfig from dataclasses import field + @dataclass class AppConfig(BaseConfig): database: DatabaseConfig = field(default_factory=DatabaseConfig) diff --git a/example/database-app/config/database.py b/example/database-app/config/database.py index a9a1998a..3c6ddd9c 100644 --- a/example/database-app/config/database.py +++ b/example/database-app/config/database.py @@ -10,20 +10,20 @@ class DatabaseConfig: default: str = field(default_factory=lambda: env("DB_CONNECTION", "mysql")) - connections: dict[str, Dict[str, Any]] = field(default_factory=lambda: { - "sqlite": SQLiteConfig( - driver="sqlite", - database=env("DB_DATABASE", "database.sqlite"), - ), - "mysql": MySQLConfig( - driver="mysql", - host=env("DB_HOST", "127.0.0.1"), - database=env("DB_DATABASE", "laravel"), - username=env("DB_USERNAME", "root"), - password=env("DB_PASSWORD", ""), - port=env("DB_PORT", "3306"), - options={ - "charset": "utf8mb4" - } - ), - }) + connections: dict[str, Dict[str, Any]] = field( + default_factory=lambda: { + "sqlite": SQLiteConfig( + driver="sqlite", + database=env("DB_DATABASE", "database.sqlite"), + ), + "mysql": MySQLConfig( + driver="mysql", + host=env("DB_HOST", "127.0.0.1"), + database=env("DB_DATABASE", "laravel"), + username=env("DB_USERNAME", "root"), + password=env("DB_PASSWORD", ""), + port=env("DB_PORT", "3306"), + options={"charset": "utf8mb4"}, + ), + } + ) diff --git a/example/database-app/config/logging.py b/example/database-app/config/logging.py index cf2fe44a..b916539c 100644 --- a/example/database-app/config/logging.py +++ b/example/database-app/config/logging.py @@ -6,18 +6,19 @@ @dataclasses.dataclass class LoggingConfig: - default: str = dataclasses.field(default_factory=lambda: env('LOG_CHANNEL', 'terminal')) + default: str = dataclasses.field( + default_factory=lambda: env("LOG_CHANNEL", "terminal") + ) - channels: dict = dataclasses.field(default_factory=lambda: { - 'stack': StackChannel( - driver='stack', - channels=['daily', 'terminal'] - ), - 'daily': DailyChannel( - level=env('LOG_DAILY_LEVEL', 'info'), - path=env('LOG_DAILY_PATH', 'storage/logs'), - ), - 'terminal': TerminalChannel( - level=env('LOG_TERMINAL_LEVEL', 'info'), - ), - }) + channels: dict = dataclasses.field( + default_factory=lambda: { + "stack": StackChannel(driver="stack", channels=["daily", "terminal"]), + "daily": DailyChannel( + level=env("LOG_DAILY_LEVEL", "info"), + path=env("LOG_DAILY_PATH", "storage/logs"), + ), + "terminal": TerminalChannel( + level=env("LOG_TERMINAL_LEVEL", "info"), + ), + } + ) diff --git a/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py b/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py index b26ffb06..7365c0aa 100644 --- a/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py @@ -12,9 +12,13 @@ async def up(self): table.increments("id") table.string("title") table.integer("instructor_id").unsigned() - table.foreign("instructor_id").references("id").on("users").on_delete("cascade") + table.foreign("instructor_id").references("id").on("users").on_delete( + "cascade" + ) table.integer("category_id").unsigned().nullable() - table.foreign("category_id").references("id").on("categories").on_delete("set null") + table.foreign("category_id").references("id").on("categories").on_delete( + "set null" + ) table.timestamps() async def down(self): diff --git a/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py b/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py index e5c0a108..88b7e18d 100644 --- a/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py @@ -13,7 +13,9 @@ async def up(self): table.integer("user_id").unsigned() table.foreign("user_id").references("id").on("users").on_delete("cascade") table.integer("course_id").unsigned() - table.foreign("course_id").references("id").on("courses").on_delete("cascade") + table.foreign("course_id").references("id").on("courses").on_delete( + "cascade" + ) table.integer("progress").default(0) table.timestamp("completed_at").nullable() table.timestamps() diff --git a/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py b/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py index 3f9582ab..649c9c9d 100644 --- a/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py @@ -11,7 +11,9 @@ async def up(self): async with await self.schema.create("lessons") as table: table.increments("id") table.integer("course_id").unsigned() - table.foreign("course_id").references("id").on("courses").on_delete("cascade") + table.foreign("course_id").references("id").on("courses").on_delete( + "cascade" + ) table.string("title") table.timestamps() diff --git a/example/database-app/databases/seeds/category_seeder.py b/example/database-app/databases/seeds/category_seeder.py index 475f58aa..4e96f2e7 100644 --- a/example/database-app/databases/seeds/category_seeder.py +++ b/example/database-app/databases/seeds/category_seeder.py @@ -12,4 +12,4 @@ async def run(self): "Business", ] for name in categories: - await Category.first_or_create({"name": name}) \ No newline at end of file + await Category.first_or_create({"name": name}) diff --git a/example/database-app/databases/seeds/course_seeder.py b/example/database-app/databases/seeds/course_seeder.py index 440c5cd5..115db945 100644 --- a/example/database-app/databases/seeds/course_seeder.py +++ b/example/database-app/databases/seeds/course_seeder.py @@ -54,6 +54,4 @@ async def run(self): for data in courses: course, _ = await Course.first_or_create({"title": data["title"]}, data) for title in lesson_map[data["title"]]: - await Lesson.first_or_create( - {"title": title, "course_id": course.id} - ) \ No newline at end of file + await Lesson.first_or_create({"title": title, "course_id": course.id}) diff --git a/example/database-app/databases/seeds/database_seeder.py b/example/database-app/databases/seeds/database_seeder.py index 15e4c682..e4840f3c 100644 --- a/example/database-app/databases/seeds/database_seeder.py +++ b/example/database-app/databases/seeds/database_seeder.py @@ -10,4 +10,4 @@ async def run(self): await self.call(CategorySeeder) await self.call(UserSeeder) await self.call(CourseSeeder) - await self.call(ReviewSeeder) \ No newline at end of file + await self.call(ReviewSeeder) diff --git a/example/database-app/databases/seeds/post_seeder.py b/example/database-app/databases/seeds/post_seeder.py index 890fd7c6..50dfb385 100644 --- a/example/database-app/databases/seeds/post_seeder.py +++ b/example/database-app/databases/seeds/post_seeder.py @@ -15,22 +15,26 @@ async def run(self): tag_database = await Tag.first_or_create({"name": "database"}) # Create First Blog Post - post1 = await Post.create({ - "user_id": user.id, - "title": "Laravel and Databases", - "content": "This is a post about Laravel framework and its database capabilities." - }) + post1 = await Post.create( + { + "user_id": user.id, + "title": "Laravel and Databases", + "content": "This is a post about Laravel framework and its database capabilities.", + } + ) # Attach tags to first post await PostTag.first_or_create({"post_id": post1.id, "tag_id": tag_laravel.id}) await PostTag.first_or_create({"post_id": post1.id, "tag_id": tag_database.id}) # Create Second Blog Post - post2 = await Post.create({ - "user_id": user.id, - "title": "FastAPI and Databases", - "content": "This is a post about FastAPI performance and database handling." - }) + post2 = await Post.create( + { + "user_id": user.id, + "title": "FastAPI and Databases", + "content": "This is a post about FastAPI performance and database handling.", + } + ) # Attach tags to second post await PostTag.first_or_create({"post_id": post2.id, "tag_id": tag_fastapi.id}) diff --git a/example/database-app/databases/seeds/review_seeder.py b/example/database-app/databases/seeds/review_seeder.py index 29dd3f22..4bfda4ce 100644 --- a/example/database-app/databases/seeds/review_seeder.py +++ b/example/database-app/databases/seeds/review_seeder.py @@ -26,5 +26,9 @@ async def run(self): contents = reviews_by_title.get(course.title, []) for content in contents: await Review.first_or_create( - {"reviewable_type": "courses", "reviewable_id": course.id, "content": content} - ) \ No newline at end of file + { + "reviewable_type": "courses", + "reviewable_id": course.id, + "content": content, + } + ) diff --git a/example/database-app/databases/seeds/user_seeder.py b/example/database-app/databases/seeds/user_seeder.py index 737612cc..2edc9982 100644 --- a/example/database-app/databases/seeds/user_seeder.py +++ b/example/database-app/databases/seeds/user_seeder.py @@ -46,5 +46,7 @@ async def run(self): for data in users: profile_data = data.pop("profile") - user= await User.first_or_create({"email": data["email"]}, data) - await Profile.first_or_create({"user_id": user.id}, {"user_id": user.id, **profile_data}) + user = await User.first_or_create({"email": data["email"]}, data) + await Profile.first_or_create( + {"user_id": user.id}, {"user_id": user.id, **profile_data} + ) diff --git a/example/database-app/providers/console_provider.py b/example/database-app/providers/console_provider.py index 160a20bc..6ee886fa 100644 --- a/example/database-app/providers/console_provider.py +++ b/example/database-app/providers/console_provider.py @@ -1,5 +1,6 @@ from fastapi_startkit.providers import Provider + class ConsoleProvider(Provider): def register(self) -> None: pass diff --git a/example/database-app/providers/fastapi_provider.py b/example/database-app/providers/fastapi_provider.py index 7d0b02f2..330c7184 100644 --- a/example/database-app/providers/fastapi_provider.py +++ b/example/database-app/providers/fastapi_provider.py @@ -1,5 +1,6 @@ from fastapi_startkit.fastapi import FastAPIProvider + class FastAPIServiceProvider(FastAPIProvider): def boot(self) -> None: super().boot() diff --git a/example/database-app/tests/features/students/test_register.py b/example/database-app/tests/features/students/test_register.py index 408d051c..7c08b77c 100644 --- a/example/database-app/tests/features/students/test_register.py +++ b/example/database-app/tests/features/students/test_register.py @@ -7,11 +7,14 @@ class TestRegister(TestCase, HttpTestCase, RefreshDatabase): async def test_user_can_register(self): - response = await self.post("/students/register", json={ - "name": "John Doe", - "email": "john@example.com", - "password": "password123", - }) + response = await self.post( + "/students/register", + json={ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + }, + ) assert response.status_code == 200 assert response.json()["message"] == "Student registered successfully" @@ -28,27 +31,36 @@ async def test_user_cannot_register_with_invalid_data(self): assert response.status_code == 422 # password to shorts - response = await self.post("/students/register", json={ - "name": "John Doe", - "email": "john@example.com", - "password": "short", - }) + response = await self.post( + "/students/register", + json={ + "name": "John Doe", + "email": "john@example.com", + "password": "short", + }, + ) assert response.status_code == 422 # invalid email - response = await self.post("/students/register", json={ - "name": "John Doe", - "email": "not-an-email", - "password": "password123", - }) + response = await self.post( + "/students/register", + json={ + "name": "John Doe", + "email": "not-an-email", + "password": "password123", + }, + ) assert response.status_code == 422 # name too shorts - response = await self.post("/students/register", json={ - "name": "J", - "email": "john@example.com", - "password": "password123", - }) + response = await self.post( + "/students/register", + json={ + "name": "J", + "email": "john@example.com", + "password": "password123", + }, + ) assert response.status_code == 422 async def test_user_cannot_register_with_duplicate_email(self): diff --git a/example/database-app/tests/features/teachers/test_register.py b/example/database-app/tests/features/teachers/test_register.py index ab4da6eb..bc0105a0 100644 --- a/example/database-app/tests/features/teachers/test_register.py +++ b/example/database-app/tests/features/teachers/test_register.py @@ -6,7 +6,12 @@ class TestRegister(RefreshDatabase, TestCase): async def test_register(self): - user = User(name="Teacher", email="teacher@example.com", password="password123", role="teacher") + user = User( + name="Teacher", + email="teacher@example.com", + password="password123", + role="teacher", + ) await user.save() found = await User.where("email", "teacher@example.com").first() @@ -17,4 +22,4 @@ async def test_register(self): class TestTableIsClean(DatabaseTransaction, TestCase): async def test_table_is_clean(self): users = await User.all() - assert len(users) == 0 \ No newline at end of file + assert len(users) == 0 diff --git a/example/database-app/tests/test_case.py b/example/database-app/tests/test_case.py index 72f59997..c3319011 100644 --- a/example/database-app/tests/test_case.py +++ b/example/database-app/tests/test_case.py @@ -8,6 +8,7 @@ class TestCase(BaseTestCase, ABC): - def get_application(self) -> 'Application': + def get_application(self) -> "Application": from bootstrap.application import app + return app