Currently there are at least 3 critical features missing from the library:
Interface support, Mutation support, and InputObjectType support that integrate with SQLAlchemy models. A last, but nice to have, would be a type that automatically generates all graphene-sqlalchemy models and correctly assigns them the appropriate Interface type based on their model.
Interface is critically important for people using polymorphic database structures. The importance of auto-generated SQLAlchemy Mutations and InputObjectTypes is self explanatory.
a SQLAlchemyInterface should have as its meta fields a name and an SQLAlchemy model, e.g.
class BaseClassModelInterface(SQLAlchemyInterface):
class Meta:
name = 'BaseClassNode'
model = BaseClassModel
Because it will act as a Node elsewhere, in my implementation I have it extend Node (but call super(AbstractNode) to specify it's meta rather than have it be overridden)
def exclude_autogenerated_sqla_columns(model: DeclarativeMeta) -> Tuple[str]:
# always pull ids out to a separate argument
autoexclude: List[str] = []
for col in sqlalchemy.inspect(model).columns:
if ((col.primary_key and col.autoincrement) or
(isinstance(col.type, sqlalchemy.types.TIMESTAMP) and
col.server_default is not None)):
autoexclude.append(col.name)
assert isinstance(col.name, str)
return tuple(autoexclude)
class SQLAlchemyInterfaceOptions(InterfaceOptions):
model = None #
registry = None #
connection = None #
id = None # type: str
class SQLAlchemyInterface(Node):
@classmethod
def __init_subclass_with_meta__(
cls,
model: DeclarativeMeta = None,
registry: Registry = None,
only_fields: Tuple[str] = (),
exclude_fields: Tuple[str] = (),
connection_field_factory: UnsortedSQLAlchemyConnectionField = default_connection_field_factory,
**options
):
_meta = SQLAlchemyInterfaceOptions(cls)
_meta.name = f'{cls.__name__}Node'
autoexclude_columns = exclude_autogenerated_sqla_columns(model=model)
exclude_fields += autoexclude_columns
assert is_mapped_class(model), (
"You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
).format(cls.__name__, model)
if not registry:
registry = get_global_registry()
assert isinstance(registry, Registry), (
"The attribute registry in {} needs to be an instance of "
'Registry, received "{}".'
).format(cls.__name__, registry)
sqla_fields = yank_fields_from_attrs(
construct_fields(
obj_type=cls,
model=model,
registry=registry,
only_fields=only_fields,
exclude_fields=exclude_fields,
connection_field_factory=connection_field_factory
),
_as=Field
)
if not _meta:
_meta = SQLAlchemyInterfaceOptions(cls)
_meta.model = model
_meta.registry = registry
connection = Connection.create_type(
"{}Connection".format(cls.__name__), node=cls)
assert issubclass(connection, Connection), (
"The connection must be a Connection. Received {}"
).format(connection.__name__)
_meta.connection = connection
if _meta.fields:
_meta.fields.update(sqla_fields)
else:
_meta.fields = sqla_fields
_meta.fields['id'] = graphene.GlobalID(cls, description="The ID of the object.")
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod
def Field(cls, *args, **kwargs): # noqa: N802
return NodeField(cls, *args, **kwargs)
@classmethod
def node_resolver(cls, only_type, root, info, id):
return cls.get_node_from_global_id(info, id, only_type=only_type)
@classmethod
def get_node_from_global_id(cls, info, global_id, only_type=None):
try:
node: DeclarativeMeta = info.context.get('session').query(cls._meta.model).filter_by(id=global_id).one_or_none()
return node
except Exception:
return None
@classmethod
def from_global_id(cls, global_id):
return global_id
@classmethod
def to_global_id(cls, type, id):
return id
@classmethod
def resolve_type(cls, instance, info):
if isinstance(instance, graphene.ObjectType):
return type(instance)
graphene_model = get_global_registry().get_type_for_model(type(instance))
if graphene_model:
return graphene_model
else:
raise ValueError(f'{instance} must be a SQLAlchemy model or graphene.ObjectType')
A mutation should take as its meta arguments the SQLAlchemy Model, it's CRUD operation . (Create Edit or Delete), and the graphene structure of its response (Output type)
class CreateFoos(SQLAlchemyMutation):
class Arguments:
foos = graphene.Argument(graphene.List(FooInput))
class Meta:
create = True
model = FooModel
structure = graphene.List
class SQLAlchemyMutation(graphene.Mutation):
@classmethod
def __init_subclass_with_meta__(cls, model=None, create=False,
delete=False, registry=None,
arguments=None, only_fields=(),
structure: Type[Structure] = None,
exclude_fields=(), **options):
meta = SQLAlchemyMutationOptions(cls)
meta.create = create
meta.model = model
meta.delete = delete
if arguments is None and not hasattr(cls, "Arguments"):
arguments = {}
# don't include id argument on create
if not meta.create:
arguments['id'] = graphene.ID(required=True)
# don't include input argument on delete
if not meta.delete:
inputMeta = type('Meta', (object,), {
'model': model,
'exclude_fields': exclude_fields,
'only_fields': only_fields
})
inputType = type(cls.__name__ + 'Input',
(SQLAlchemyInputObjectType,),
{'Meta': inputMeta})
arguments = {'input': inputType(required=True)}
if not registry:
registry = get_global_registry()
output_type: ObjectType = registry.get_type_for_model(model)
if structure:
output_type = structure(output_type)
super(SQLAlchemyMutation, cls).__init_subclass_with_meta__(_meta=meta, output=output_type, arguments=arguments, **options)
@classmethod
def mutate(cls, info, **kwargs):
session = get_session(info.context)
with session.no_autoflush:
meta = cls._meta
model = None
if meta.create:
model = meta.model(**kwargs['input'])
session.add(model)
else:
model = session.query(meta.model).filter(meta.model.id == kwargs['id']).first()
if meta.delete:
session.delete(model)
else:
def setModelAttributes(model, attrs):
relationships = model.__mapper__.relationships
for key, value in attrs.items():
if key in relationships:
if getattr(model, key) is None:
# instantiate class of the same type as
# the relationship target
setattr(model, key,
relationships[key].mapper.entity())
setModelAttributes(getattr(model, key), value)
else:
setattr(model, key, value)
setModelAttributes(model, kwargs['input'])
session.commit()
return model
@classmethod
def Field(cls, *args, **kwargs):
return graphene.Field(cls._meta.output,
args=cls._meta.arguments,
resolver=cls._meta.resolver)
an SQLAlchemy InputObjectType should introspect the sqla model and autogenerate fields to select based upon and set the appropriate field data type:
e.g.
class Bar(SQLAlchemyInputObjectType):
class Meta:
model = BarModel
exclude_fields = ( 'polymorphic_discriminator', 'active', 'visible_id')
```python
class SQLAlchemyInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(
cls,
model=None,
registry=None,
skip_registry=False,
only_fields=(),
exclude_fields=(),
connection=None,
connection_class=None,
use_connection=None,
interfaces=(),
id=None,
connection_field_factory=default_connection_field_factory,
_meta=None,
**options
):
autoexclude = []
# always pull ids out to a separate argument
for col in sqlalchemy.inspect(model).columns:
if ((col.primary_key and col.autoincrement) or
(isinstance(col.type, sqlalchemy.types.TIMESTAMP) and
col.server_default is not None)):
autoexclude.append(col.name)
if not registry:
registry = get_global_registry()
sqla_fields = yank_fields_from_attrs(
construct_fields(cls, model, registry, only_fields, exclude_fields + tuple(autoexclude), connection_field_factory),
_as=Field,
)
# create accessor for model to be retrieved for querying
cls.sqla_model = model
if use_connection is None and interfaces:
use_connection = any(
(issubclass(interface, Node) for interface in interfaces)
)
if use_connection and not connection:
# We create the connection automatically
if not connection_class:
connection_class = Connection
connection = connection_class.create_type(
"{}Connection".format(cls.__name__), node=cls
)
if connection is not None:
assert issubclass(connection, Connection), (
"The connection must be a Connection. Received {}"
).format(connection.__name__)
for key, value in sqla_fields.items():
if not (isinstance(value, Dynamic) or hasattr(cls, key)):
setattr(cls, key, value)
super(SQLAlchemyInputObjectType, cls).__init_subclass_with_meta__(**options)
Currently there are at least 3 critical features missing from the library:
Interface support, Mutation support, and InputObjectType support that integrate with SQLAlchemy models. A last, but nice to have, would be a type that automatically generates all graphene-sqlalchemy models and correctly assigns them the appropriate Interface type based on their model.
Interface is critically important for people using polymorphic database structures. The importance of auto-generated SQLAlchemy Mutations and InputObjectTypes is self explanatory.
a SQLAlchemyInterface should have as its meta fields a name and an SQLAlchemy model, e.g.
Because it will act as a Node elsewhere, in my implementation I have it extend Node (but call super(AbstractNode) to specify it's meta rather than have it be overridden)
A mutation should take as its meta arguments the SQLAlchemy Model, it's CRUD operation . (Create Edit or Delete), and the graphene structure of its response (Output type)
an SQLAlchemy InputObjectType should introspect the sqla model and autogenerate fields to select based upon and set the appropriate field data type:
e.g.