diff --git a/playground.py b/playground.py new file mode 100644 index 00000000..22efc298 --- /dev/null +++ b/playground.py @@ -0,0 +1,116 @@ +class Database: + """ + Simulate a database query. Replace this with actual database calls. + """ + @staticmethod + def get(table, conditions): + # Dummy data for demonstration + data = { + "companies": [{"company_id": 1, "name": "Acme Corp"}], + "users": [{"user_id": 1, "company_id": 1, "name": "John Doe"}], + } + return data['companies'][0] + + +class HasOne: + def __init__(self, parent, related_model, foreign_key, local_key): + self.parent = parent + self.related_model = related_model + self.foreign_key = foreign_key + self.local_key = local_key + self._related_instance = None + + def get(self): + """ + Perform the database query to fetch the related model. + """ + if self._related_instance is None: + related_data = Database.get( + self.related_model.table_name(), + {self.foreign_key: getattr(self.parent, self.local_key)} + ) + if related_data: + self._related_instance = self.related_model(**related_data) + return self._related_instance + + def __call__(self): + """ + Allow the relationship instance to be callable. + """ + return self + + +class RelationshipProperty: + """ + A wrapper for dual behavior: as a property and as a callable returning the relationship instance. + """ + def __init__(self, relationship): + self.relationship = relationship + + def __getattr__(self, name): + """ + Delegate attribute access to the related model instance. + """ + print("Delegating attribute access to the related model instance") + related_instance = self.relationship.get() + if related_instance: + return getattr(related_instance, name) + raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'") + + def __call__(self): + """ + Make the relationship callable to return the relationship instance. + """ + return self.relationship + + def __repr__(self): + return repr(self.relationship.get()) + + + + +class Model: + """ + Base model class to share common functionality. + """ + @classmethod + def table_name(cls): + return cls.__name__.lower() + "s" # Example: User -> users + + def has_one(self, related_model, foreign_key=None, local_key="id"): + return RelationshipProperty(HasOne(self, related_model, foreign_key, local_key)) + + +class Company(Model): + def __init__(self, company_id, name): + self.company_id = company_id + self.name = name + + def welcome(self): + return f"Welcome to {self.name}" + +class User(Model): + def __init__(self, user_id, company_id, name): + self.user_id = user_id + self.company_id = company_id + self.name = name + + @property + def company(self): + """ + Return a RelationshipProperty wrapper for dual behavior. + """ + return self.has_one(Company, "company_id", "company_id") + + +# Usage +user = User(user_id=1, company_id=1, name="John Doe") + +# Access the related company instance as a property +related_company = user.company +print("property", related_company) # Output: Acme Corp +print("property", related_company.name, related_company.name=="Acme Corp") # Output: Acme Corp + +# Call the relationship instance to get the related company +related_company_callable = user.company() +print("callable", isinstance(related_company_callable, HasOne)) # Output: True diff --git a/playground2.py b/playground2.py new file mode 100644 index 00000000..6ef0d07a --- /dev/null +++ b/playground2.py @@ -0,0 +1,47 @@ +from src.masoniteorm.query import QueryBuilder +from src.masoniteorm.models import Model +from src.masoniteorm.models.relationships import HasOne + + +class CreditCard(Model): + __table__ = "company_credit_cards" + __primary_key__ = "credit_card_id" + + +class Company(Model): + __table__ = "tbl_companies" + + @property + def cards(self): + """ + Return a RelationshipProperty wrapper for dual behavior. + """ + return self.has_many(CreditCard, "company_id", "company_id") + +class User(Model): + __table__ = "tbl_users" + __primary_key__ = "user_id" + + @property + def company(self): + """ + Return a RelationshipProperty wrapper for dual behavior. + """ + return self.has_one(Company, "company_id", "company_id") + + +# Usage +# user = User(user_id=1, company_id=1, name="John Doe") +user = User.find(667) + +# Access the related company instance as a property +# related_company = user.company +# print("property name", related_company.company_name) # Output: Acme Corp +# print("property 2", related_company.company_name, related_company.company_name=="Acme Corp") # Output: Acme Corp + +# # Call the relationship instance to get the related company +# related_company_callable = user.company().get() +company = user.company +print(company.cards().to_sql()) +# print("callable", isinstance(user.company(), HasOne)) # Output: True + diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 35967eeb..8b1444c7 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -16,12 +16,11 @@ from ..observers import ObservesEvents from ..query import QueryBuilder from ..scopes import TimeStampsMixin +from .relationships.new import HasOne, HasMany """This is a magic class that will help using models like User.first() instead of having to instatiate a class like User().first() """ - - class ModelMeta(type): def __getattr__(self, attribute, *args, **kwargs): """This method is called between a Model and accessing a property. This is a quick and easy @@ -1175,3 +1174,37 @@ def filter_guarded(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: # If all fields are guarded, all data should be filtered return {} return {f: dictionary[f] for f in dictionary if f not in cls.__guarded__} + + + # move to relationships class + def has_one(self, related_model, foreign_key=None, local_key="id"): + return RelationshipProperty(HasOne(self, related_model, foreign_key, local_key)) + + def has_many(self, related_model, foreign_key=None, local_key="id"): + print("setting up has many") + return RelationshipProperty(HasMany(self, related_model, foreign_key, local_key)) +class RelationshipProperty: + """ + A wrapper for dual behavior: as a property and as a callable returning the relationship instance. + """ + def __init__(self, relationship): + self.relationship = relationship + + def __getattr__(self, name): + """ + Delegate attribute access to the related model instance. + """ + related_instance = self.relationship.get() + if related_instance: + return getattr(related_instance, name) + raise AttributeError(f"{self.__class__.__name__} has no attribute from relation '{name}'") + + def __call__(self): + """ + Make the relationship callable to return the relationship instance. + """ + print("Calling relationship") + return self.relationship.apply_query() + + # def __repr__(self): + # return repr(self.relationship) \ No newline at end of file diff --git a/src/masoniteorm/models/__init__.py b/src/masoniteorm/models/__init__.py index 0188f11a..cdb3fe74 100644 --- a/src/masoniteorm/models/__init__.py +++ b/src/masoniteorm/models/__init__.py @@ -1 +1 @@ -from .Model import Model +from .Model import Model \ No newline at end of file diff --git a/src/masoniteorm/models/relationships/HasOne.py b/src/masoniteorm/models/relationships/HasOne.py new file mode 100644 index 00000000..50ea8556 --- /dev/null +++ b/src/masoniteorm/models/relationships/HasOne.py @@ -0,0 +1,27 @@ +class HasOne: + def __init__(self, parent, related_model, foreign_key, local_key): + self.parent = parent + self.related_model = related_model + self.foreign_key = foreign_key + self.local_key = local_key + self._related_instance = None + + def get(self): + """ + Perform the database query to fetch the related model. + """ + print("getting") + if self._related_instance is None: + self._related_instance = self.related_model.where(self.foreign_key, getattr(self.parent, self.foreign_key)).first() + + return self._related_instance + + def __call__(self): + """ + Allow the relationship instance to be callable. + """ + print("calling") + return self + + def where(self, key, value): + return self.related_model.where(key, value) \ No newline at end of file diff --git a/src/masoniteorm/models/relationships/__init__.py b/src/masoniteorm/models/relationships/__init__.py new file mode 100644 index 00000000..6878ab7c --- /dev/null +++ b/src/masoniteorm/models/relationships/__init__.py @@ -0,0 +1 @@ +from .HasOne import HasOne \ No newline at end of file diff --git a/src/masoniteorm/models/relationships/new/BelongsTo.py b/src/masoniteorm/models/relationships/new/BelongsTo.py new file mode 100644 index 00000000..e69de29b diff --git a/src/masoniteorm/models/relationships/new/HasMany.py b/src/masoniteorm/models/relationships/new/HasMany.py new file mode 100644 index 00000000..48b130dc --- /dev/null +++ b/src/masoniteorm/models/relationships/new/HasMany.py @@ -0,0 +1,40 @@ +class HasMany: + def __init__(self, parent, related_model, foreign_key, local_key): + self.parent = parent + self.related_model = related_model + self.foreign_key = foreign_key + self.local_key = local_key + self._related_instance = None + + def get(self): + """ + Perform the database query to fetch the related model. + """ + print("getting has many") + if self._related_instance is None: + self._related_instance = self.apply_query().get() + + return self._related_instance + + def __call__(self): + """ + Allow the relationship instance to be callable. + """ + print("calling") + return self.relationship.apply_query().get() + + def where(self, key): + return self.related_model.where(key, value) + + def apply_query(self): + """Apply the query and return a dictionary to be hydrated + + Arguments: + foreign {oject} -- The relationship object + owner {object} -- The current model oject. + + Returns: + dict -- A dictionary of data which will be hydrated. + """ + print("applying query has many") + return self.related_model.where(self.foreign_key, getattr(self.parent, self.foreign_key)) \ No newline at end of file diff --git a/src/masoniteorm/models/relationships/new/HasOne.py b/src/masoniteorm/models/relationships/new/HasOne.py new file mode 100644 index 00000000..79a3f241 --- /dev/null +++ b/src/masoniteorm/models/relationships/new/HasOne.py @@ -0,0 +1,41 @@ +class HasOne: + def __init__(self, parent, related_model, foreign_key, local_key): + self.parent = parent + self.related_model = related_model + self.foreign_key = foreign_key + self.local_key = local_key + self._related_instance = None + + def get(self): + """ + Perform the database query to fetch the related model. + """ + if self._related_instance is None: + self._related_instance = self.apply_query().first() + + return self._related_instance + + def __call__(self): + """ + Allow the relationship instance to be callable. + """ + return self + + def __repr__(self): + return repr(self.related_model) + + def where(self, key): + return self.related_model.where(key, value) + + def apply_query(self): + """Apply the query and return a dictionary to be hydrated + + Arguments: + foreign {oject} -- The relationship object + owner {object} -- The current model oject. + + Returns: + dict -- A dictionary of data which will be hydrated. + """ + + return self.related_model.where(self.foreign_key, getattr(self.parent, self.foreign_key)) \ No newline at end of file diff --git a/src/masoniteorm/models/relationships/new/RelationshipProperty.py b/src/masoniteorm/models/relationships/new/RelationshipProperty.py new file mode 100644 index 00000000..e69de29b diff --git a/src/masoniteorm/models/relationships/new/__init__.py b/src/masoniteorm/models/relationships/new/__init__.py new file mode 100644 index 00000000..22b1f769 --- /dev/null +++ b/src/masoniteorm/models/relationships/new/__init__.py @@ -0,0 +1,3 @@ +from .HasOne import HasOne +from .HasMany import HasMany +# from .BelongsTo import BelongsTo \ No newline at end of file