diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py index a4ea9e9c..ac7bbed1 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py @@ -82,20 +82,17 @@ def handle(self) -> int: self.info(f"Model created: {os.path.join(model_directory, file_name)}") if self.option("migration"): - migrations_directory = self.option("migrations-directory") + table = tableize(name) if self.option("table"): - self.call( - "migration", - f"update_{tableize(name)}_table --table {tableize(name)} --directory {migrations_directory}", - ) + arguments = f"update_{table}_table --table {table}" else: - self.call( - "migration", - f"create_{tableize(name)}_table --create {tableize(name)} --directory {migrations_directory}", - ) + arguments = f"create_{table}_table --create {table}" + self.call_command("db:make:migration", f"{arguments} --directory {self.option('migrations-directory')}") if self.option("seeder"): - directory = self.option("seeders-directory") - self.call("seed", f"{self.argument('name')} --directory {directory}") + self.call_command("seed", f"{name} --directory {self.option('seeders-directory')}") return 0 + + def call_command(self, name: str, arguments: str) -> int: + return self.call(name, f"{name} {arguments}") diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py index 12601b79..98cafd74 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py @@ -6,7 +6,7 @@ class MakeObserverCommand(Command): - name = "observer" + name = "db:make:observer" description = "Creates a new observer file." arguments = [argument("name", description="The name of the observer")] diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/model.stub b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/model.stub index 6942517d..a5fd3c75 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/model.stub +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/model.stub @@ -1,9 +1,5 @@ -""" __CLASS__ Model """ - -from masoniteorm.models import Model +from fastapi_startkit.masoniteorm.models import Model class __CLASS__(Model): - """__CLASS__ Model""" - pass diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/observer.stub b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/observer.stub index 9bdff7d4..7e24ccb0 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/observer.stub +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/observer.stub @@ -1,101 +1,27 @@ -"""__CLASS__ Observer""" - -from masoniteorm.models import Model +from fastapi_startkit.masoniteorm.models import Model class __CLASS__Observer: - def created(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "created" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def saving(self, __MODEL_VARIABLE__: Model) -> None: pass - def creating(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "creating" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def creating(self, __MODEL_VARIABLE__: Model) -> None: pass - def saving(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "saving" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def updating(self, __MODEL_VARIABLE__: Model) -> None: pass - def saved(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "saved" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def created(self, __MODEL_VARIABLE__: Model) -> None: pass - def updating(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "updating" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def updated(self, __MODEL_VARIABLE__: Model) -> None: pass - def updated(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "updated" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def saved(self, __MODEL_VARIABLE__: Model) -> None: pass - def booted(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "booted" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ - pass - - def booting(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "booting" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def deleting(self, __MODEL_VARIABLE__: Model) -> None: pass - def hydrating(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "hydrating" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ - pass - - def hydrated(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "hydrated" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ - pass - - def deleting(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "deleting" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ - pass - - def deleted(self, __MODEL_VARIABLE__): - """Handle the __MODEL__ "deleted" event. - - Args: - __MODEL_VARIABLE__ (masoniteorm.models.Model): __MODEL__ model. - """ + async def deleted(self, __MODEL_VARIABLE__: Model) -> None: pass diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py index 7b90dada..2aea8551 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py @@ -9,6 +9,7 @@ DBSeedCommand, MakeMigrationCommand, MakeModelCommand, + MakeObserverCommand, MakeSeedCommand, MigrateRollbackCommand, MigrateFreshCommand, @@ -59,6 +60,7 @@ def boot(self) -> None: MigrateStatusCommand, MigrateRollbackCommand, MakeModelCommand, + MakeObserverCommand, MakeSeedCommand, MigrateFreshCommand, MigrateRefreshCommand, diff --git a/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py b/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py index d479afa8..b8b7c6b9 100644 --- a/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py +++ b/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py @@ -13,6 +13,7 @@ from fastapi_startkit.masoniteorm.commands.MakeObserverCommand import MakeObserverCommand from fastapi_startkit.masoniteorm.commands.MakeSeedCommand import MakeSeedCommand from fastapi_startkit.masoniteorm.migrations.Migration import Migration +from fastapi_startkit.masoniteorm.models import Model from fastapi_startkit.masoniteorm.seeders import Seeder from .fixtures.app import create_app, DB_PATH @@ -37,6 +38,12 @@ def tearDown(self): def _tester(self, command_class): return CommandTester(command_class()) + def _load_class(self, path, class_name): + spec = importlib.util.spec_from_file_location(class_name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return getattr(module, class_name) + def _generated_file(self, directory): files = [f for f in os.listdir(directory) if f.endswith(".py")] self.assertEqual(len(files), 1, f"expected one generated file in {directory}, got {files}") @@ -189,6 +196,50 @@ def test_creates_model_file(self): content = open(os.path.join("app", "Post.py")).read() self.assertIn("class Post(Model)", content) + def test_generated_model_imports(self): + os.makedirs("app") + self._tester(MakeModelCommand).execute("Post") + + content = open(os.path.join("app", "Post.py")).read() + self.assertIn("from fastapi_startkit.masoniteorm.models import Model", content) + self.assertNotIn('"""', content) + + post_class = self._load_class(os.path.join("app", "Post.py"), "Post") + self.assertTrue(issubclass(post_class, Model)) + + def test_migration_option_creates_migration_through_console(self): + os.makedirs("app") + os.makedirs("databases/migrations") + app = create_app() + + self.assertEqual(app.run("db:make:model", "Post --migration"), 0) + + migration = self._generated_file("databases/migrations") + self.assertTrue(migration.endswith("_create_posts_table.py")) + content = open(migration).read() + self.assertIn('self.schema.create("posts")', content) + + def test_migration_and_table_options_create_update_migration_through_console(self): + os.makedirs("app") + os.makedirs("databases/migrations") + app = create_app() + + self.assertEqual(app.run("db:make:model", "Post --migration --table"), 0) + + migration = self._generated_file("databases/migrations") + self.assertTrue(migration.endswith("_update_posts_table.py")) + content = open(migration).read() + self.assertIn('self.schema.table("posts")', content) + + def test_seeder_option_creates_seeder_through_console(self): + os.makedirs("app") + os.makedirs("databases/seeders") + app = create_app() + + self.assertEqual(app.run("db:make:model", "Post --seeder"), 0) + + self.assertTrue(os.path.exists(os.path.join("databases/seeders", "post_table_seeder.py"))) + def test_pep_option_uses_underscore_filename(self): os.makedirs("app") tester = self._tester(MakeModelCommand) @@ -263,8 +314,8 @@ def _forget_modules(package): class TestMakeObserverCommand(_TempCwdTestCase): - # make:observer emits a plain observer class with no schema side effects, so - # the generated file content is the only meaningful contract to assert. + LIFECYCLE_EVENTS = ["saving", "creating", "updating", "created", "updated", "saved", "deleting", "deleted"] + def test_creates_observer_file(self): tester = self._tester(MakeObserverCommand) tester.execute("Post") @@ -273,17 +324,40 @@ def test_creates_observer_file(self): self.assertIn("Observer created:", output) content = open(os.path.join("app/observers", "PostObserver.py")).read() + self.assertIn("from fastapi_startkit.masoniteorm.models import Model", content) self.assertIn("class PostObserver:", content) - self.assertIn("def created(self, post):", content) + self.assertIn("async def created(self, post: Model) -> None:", content) + self.assertNotIn('"""', content) + self.assertNotIn("#", content) - def test_model_option_controls_variable_and_type(self): + def test_model_option_controls_variable_name(self): tester = self._tester(MakeObserverCommand) tester.execute("Audit --model User") content = open(os.path.join("app/observers", "AuditObserver.py")).read() self.assertIn("class AuditObserver:", content) - self.assertIn("def created(self, user):", content) - self.assertIn("User model.", content) + self.assertIn("async def created(self, user: Model) -> None:", content) + + def test_generated_observer_imports_and_handles_lifecycle_events(self): + self._tester(MakeObserverCommand).execute("Post") + + observer_class = self._load_class(os.path.join("app/observers", "PostObserver.py"), "PostObserver") + handlers = [name for name in vars(observer_class) if not name.startswith("_")] + self.assertEqual(handlers, self.LIFECYCLE_EVENTS) + + class Post(Model): + __observers__ = {} + + Post.observe(observer_class()) + post = Post() + for event in self.LIFECYCLE_EVENTS: + asyncio.run(post.observe_events(post, event)) + + def test_runs_through_console_as_db_make_observer(self): + app = create_app() + + self.assertEqual(app.run("db:make:observer", "Order"), 0) + self.assertTrue(os.path.exists(os.path.join("app/observers", "OrderObserver.py"))) def test_reports_when_observer_already_exists(self): self._tester(MakeObserverCommand).execute("Post")