octabytes / FireO

Google Cloud Firestore modern and simplest convenient ORM package in Python. FireO is specifically designed for the Google's Firestore
https://fireo.octabyte.io
Apache License 2.0
247 stars 29 forks source link

No support for a list of reference fields #154

Closed ttiimmaahh closed 1 year ago

ttiimmaahh commented 2 years ago

Firestore supports the ability to create an array (list) of reference fields. I'm not 100% sure if this is a limitation of the google firestore library or if its on the fireo side, but I'm unable to do that via code. I'm able to do it manually via the GCP console, but querying all of the objects fails just as it does when trying to create that object scenario.

Has anyone come across this before?

` class MyFirstModel(Model): field1 = TextField() field2 = TextField()

class MySecondModel(Model): field1 = TextField() field2 = ListField(ReferenceField(MyFirstModel))`

maazullah96 commented 1 year ago

Hi @ttiimmaahh I am also trying List of Reference Fields and getting the error .Did you got any solution for it ? TypeError: ('Cannot convert to a Firestore Value', <__main__.Keyword object at 0x00000210CDD8F7F0>, 'Invalid type', <class '__main__.Keyword'>)

AxeemHaider commented 1 year ago

If ListField is not fullfil your requirement then you can extend it Create Custom Field You can also use Base Field

ADR-007 commented 1 year ago

Hi! I implemented it in the next way:

class TypedListField(ListField):
    allowed_attributes = ['nested_field']
    nested_field: Field

    def __init__(self, nested_field, *args, **kwargs):
        if not isinstance(nested_field, Field):
            raise AttributeTypeError('"nested_field" must be an instance of "Field".')

        super().__init__(*args, **kwargs, nested_field=nested_field)

    def contribute_to_model(self, model_cls, name):
        super().contribute_to_model(model_cls, name)
        self.raw_attributes['nested_field'].name = name
        self.raw_attributes['nested_field'].model_cls = model_cls

    def attr_nested_field(self, attr_val, field_val):
        return field_val

    def db_value(self, val: list[Any] | None) -> list[Any] | None:
        """Serialize using nested field."""
        serialized = super().db_value(val)
        if serialized is not None:
            serialized = [
                self.raw_attributes['nested_field'].db_value(item)
                for item in serialized
            ]

        return serialized

    def field_value(self, val: list[Any] | None) -> list[Any] | None:
        """Deserialize enum from value"""
        parsed = super().field_value(val)

        if parsed is not None:
            parsed = [
                self.raw_attributes['nested_field'].field_value(item)
                for item in parsed
            ]

        return parsed

But I would prefer to have it out of the box... I can create PR with changes to original ListField

ADR-007 commented 1 year ago

And list of models:


class NestedListField(ListField):
    allowed_attributes = ['nested_model_class']
    nested_model_class: Model

    def __init__(self, nested_model_class, *args, **kwargs):
        if not issubclass(nested_model_class, Model):
            raise AttributeTypeError('"nested_model_class" must be a subclass of "Model".')

        super().__init__(*args, **kwargs, nested_model_class=nested_model_class)

    def attr_nested_model_class(self, attr_val, field_val):
        return field_val

    def get_value(self, val, ignore_required=False, ignore_default=False):
        items: list[Model] = super().get_value(val, ignore_required, ignore_default)

        results = []
        for item in items:
            item_dict = {}
            for nested_field in self.raw_attributes['nested_model_class']._meta.field_list.values():
                nested_field: Field
                nested_field_value = getattr(item, nested_field.name)
                value = nested_field.get_value(nested_field_value, ignore_required, ignore_default)
                if value is not None:
                    item_dict[nested_field.db_column_name] = value

            results.append(item_dict)

        return results

    def field_value(self, val):
        parsed = super().field_value(val)

        if not parsed:
            return parsed

        parsed = [
            ModelWrapper.from_query_result(
                self.raw_attributes['nested_model_class'](),
                item,
                True,
            )
            for item in parsed
        ]

        return parsed
AxeemHaider commented 1 year ago

Thanks for this implementation. PR are welcomes, You can create a PR

ADR-007 commented 1 year ago

Here is the PR :)

I'm also going to change implementation of NestedModel to allow it to use inside ListField and do some other minor changes