mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 20:54:16 +03:00
Fixing it up to use the declarative API
This commit is contained in:
parent
487543206c
commit
9a0b33ca77
|
@ -2,16 +2,18 @@ from sqlalchemy import types
|
||||||
from sqlalchemy.orm import interfaces
|
from sqlalchemy.orm import interfaces
|
||||||
from singledispatch import singledispatch
|
from singledispatch import singledispatch
|
||||||
|
|
||||||
from graphene.contrib.sqlalchemy.fields import ConnectionOrListField, SQLAlchemyModelField
|
from ...core.types.scalars import Boolean, Float, ID, Int, String
|
||||||
from graphene.core.fields import BooleanField, FloatField, IDField, IntField, StringField
|
from .fields import ConnectionOrListField, SQLAlchemyModelField
|
||||||
|
|
||||||
|
|
||||||
def convert_sqlalchemy_relationship(relationship):
|
def convert_sqlalchemy_relationship(relationship):
|
||||||
model_field = SQLAlchemyModelField(field.table, description=relationship.key)
|
direction = relationship.direction
|
||||||
if relationship.direction == interfaces.ONETOMANY:
|
model = relationship.mapper.entity
|
||||||
|
model_field = SQLAlchemyModelField(model, description=relationship.doc)
|
||||||
|
if direction == interfaces.MANYTOONE:
|
||||||
return model_field
|
return model_field
|
||||||
elif (relationship.direction == interfaces.MANYTOONE or
|
elif (direction == interfaces.ONETOMANY or
|
||||||
relationship.direction == interfaces.MANYTOMANY):
|
direction == interfaces.MANYTOMANY):
|
||||||
return ConnectionOrListField(model_field)
|
return ConnectionOrListField(model_field)
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,43 +21,43 @@ def convert_sqlalchemy_column(column):
|
||||||
try:
|
try:
|
||||||
return convert_sqlalchemy_type(column.type, column)
|
return convert_sqlalchemy_type(column.type, column)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise
|
raise Exception(
|
||||||
|
"Don't know how to convert the SQLAlchemy field %s (%s)" % (column, column.__class__))
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
def convert_sqlalchemy_type():
|
def convert_sqlalchemy_type(type, column):
|
||||||
raise Exception(
|
raise Exception()
|
||||||
"Don't know how to convert the SQLAlchemy column %s (%s)" % (column, column.__class__))
|
|
||||||
|
|
||||||
|
|
||||||
@convert_sqlalchemy_type.register(types.Date)
|
@convert_sqlalchemy_type.register(types.Date)
|
||||||
@convert_sqlalchemy_type.register(types.DateTime)
|
@convert_sqlalchemy_type.register(types.DateTime)
|
||||||
@convert_sqlalchemy_type.register(types.Time)
|
@convert_sqlalchemy_type.register(types.Time)
|
||||||
@convert_sqlalchemy_type.register(types.Text)
|
|
||||||
@convert_sqlalchemy_type.register(types.String)
|
@convert_sqlalchemy_type.register(types.String)
|
||||||
|
@convert_sqlalchemy_type.register(types.Text)
|
||||||
@convert_sqlalchemy_type.register(types.Unicode)
|
@convert_sqlalchemy_type.register(types.Unicode)
|
||||||
@convert_sqlalchemy_type.register(types.UnicodeText)
|
@convert_sqlalchemy_type.register(types.UnicodeText)
|
||||||
@convert_sqlalchemy_type.register(types.Enum)
|
@convert_sqlalchemy_type.register(types.Enum)
|
||||||
def convert_column_to_string(type, column):
|
def convert_column_to_string(type, column):
|
||||||
return StringField(description=column.description)
|
return String(description=column.doc)
|
||||||
|
|
||||||
|
|
||||||
@convert_sqlalchemy_type.register(types.SmallInteger)
|
@convert_sqlalchemy_type.register(types.SmallInteger)
|
||||||
@convert_sqlalchemy_type.register(types.BigInteger)
|
@convert_sqlalchemy_type.register(types.BigInteger)
|
||||||
@convert_sqlalchemy_type.register(types.Integer)
|
@convert_sqlalchemy_type.register(types.Integer)
|
||||||
def convert_column_to_int_or_id(column):
|
def convert_column_to_int_or_id(type, column):
|
||||||
if column.primary_key:
|
if column.primary_key:
|
||||||
return IDField(description=column.description)
|
return ID(description=column.doc)
|
||||||
else:
|
else:
|
||||||
return IntField(description=column.description)
|
return Int(description=column.doc)
|
||||||
|
|
||||||
|
|
||||||
@convert_sqlalchemy_type.register(types.Boolean)
|
@convert_sqlalchemy_type.register(types.Boolean)
|
||||||
def convert_column_to_boolean(column):
|
def convert_column_to_boolean(type, column):
|
||||||
return BooleanField(description=column.description)
|
return Boolean(description=column.doc)
|
||||||
|
|
||||||
|
|
||||||
@convert_sqlalchemy_type.register(types.Float)
|
@convert_sqlalchemy_type.register(types.Float)
|
||||||
@convert_sqlalchemy_type.register(types.Numeric)
|
@convert_sqlalchemy_type.register(types.Numeric)
|
||||||
def convert_column_to_float(column):
|
def convert_column_to_float(type, column):
|
||||||
return FloatField(description=column.description)
|
return Float(description=column.doc)
|
||||||
|
|
|
@ -1,67 +1,69 @@
|
||||||
from graphene import relay
|
from sqlalchemy.orm import Query
|
||||||
from graphene.contrib.sqlalchemy.utils import get_type_for_model, lazy_map
|
from ...core.exceptions import SkipField
|
||||||
from graphene.core.fields import Field, LazyField, ListField
|
from ...core.fields import Field
|
||||||
from graphene.relay.utils import is_node
|
from ...core.types.base import FieldType
|
||||||
|
from ...core.types.definitions import List
|
||||||
|
from ...relay import ConnectionField
|
||||||
|
from ...relay.utils import is_node
|
||||||
|
from ...utils import LazyMap
|
||||||
|
|
||||||
|
from .utils import get_type_for_model
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyConnectionField(relay.ConnectionField):
|
class SQLAlchemyConnectionField(ConnectionField):
|
||||||
|
|
||||||
def wrap_resolved(self, value, instance, args, info):
|
def wrap_resolved(self, value, instance, args, info):
|
||||||
schema = info.schema.graphene_schema
|
if isinstance(value, Query):
|
||||||
return lazy_map(value, self.get_object_type(schema))
|
return LazyMap(value, self.type)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class LazyListField(ListField):
|
class LazyListField(Field):
|
||||||
|
|
||||||
def resolve(self, instance, args, info):
|
def get_type(self, schema):
|
||||||
schema = info.schema.graphene_schema
|
return List(self.type)
|
||||||
resolved = super(LazyListField, self).resolve(instance, args, info)
|
|
||||||
return lazy_map(resolved, self.get_object_type(schema))
|
def resolver(self, instance, args, info):
|
||||||
|
resolved = super(LazyListField, self).resolver(instance, args, info)
|
||||||
|
return LazyMap(resolved, self.type)
|
||||||
|
|
||||||
|
|
||||||
class ConnectionOrListField(LazyField):
|
class ConnectionOrListField(Field):
|
||||||
|
|
||||||
def get_field(self, schema):
|
def internal_type(self, schema):
|
||||||
model_field = self.field_type
|
model_field = self.type
|
||||||
field_object_type = model_field.get_object_type(schema)
|
field_object_type = model_field.get_object_type(schema)
|
||||||
|
if not field_object_type:
|
||||||
|
raise SkipField()
|
||||||
if is_node(field_object_type):
|
if is_node(field_object_type):
|
||||||
field = SQLAlchemyConnectionField(model_field)
|
field = SQLAlchemyConnectionField(field_object_type)
|
||||||
else:
|
else:
|
||||||
field = LazyListField(model_field)
|
field = LazyListField(field_object_type)
|
||||||
field.contribute_to_class(self.object_type, self.name)
|
field.contribute_to_class(self.object_type, self.attname)
|
||||||
return field
|
return schema.T(field)
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyModelField(Field):
|
class SQLAlchemyModelField(FieldType):
|
||||||
|
|
||||||
def __init__(self, model, *args, **kwargs):
|
def __init__(self, model, *args, **kwargs):
|
||||||
super(SQLAlchemyModelField, self).__init__(None, *args, **kwargs)
|
|
||||||
self.model = model
|
self.model = model
|
||||||
|
super(SQLAlchemyModelField, self).__init__(*args, **kwargs)
|
||||||
def resolve(self, instance, args, info):
|
|
||||||
resolved = super(SQLAlchemyModelField, self).resolve(instance, args, info)
|
|
||||||
schema = info.schema.graphene_schema
|
|
||||||
_type = self.get_object_type(schema)
|
|
||||||
assert _type, ("Field %s cannot be retrieved as the "
|
|
||||||
"ObjectType is not registered by the schema" % (
|
|
||||||
self.attname
|
|
||||||
))
|
|
||||||
return _type(resolved)
|
|
||||||
|
|
||||||
def internal_type(self, schema):
|
def internal_type(self, schema):
|
||||||
_type = self.get_object_type(schema)
|
_type = self.get_object_type(schema)
|
||||||
if not _type and self.object_type._meta.only_fields:
|
if not _type and self.parent._meta.only_fields:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Model %r is not accessible by the schema. "
|
"Table %r is not accessible by the schema. "
|
||||||
"You can either register the type manually "
|
"You can either register the type manually "
|
||||||
"using @schema.register. "
|
"using @schema.register. "
|
||||||
"Or disable the field %s in %s" % (
|
"Or disable the field in %s" % (
|
||||||
self.model,
|
self.model,
|
||||||
self.attname,
|
self.parent,
|
||||||
self.object_type
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return schema.T(_type) or Field.SKIP
|
if not _type:
|
||||||
|
raise SkipField()
|
||||||
|
return schema.T(_type)
|
||||||
|
|
||||||
def get_object_type(self, schema):
|
def get_object_type(self, schema):
|
||||||
return get_type_for_model(schema, self.model)
|
return get_type_for_model(schema, self.model)
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from sqlalchemy import Table
|
from sqlalchemy.ext.declarative.api import DeclarativeMeta
|
||||||
|
|
||||||
from graphene.core.options import Options
|
from ...core.options import Options
|
||||||
from graphene.relay.types import Node
|
from ...relay.types import Node
|
||||||
from graphene.relay.utils import is_node
|
from ...relay.utils import is_node
|
||||||
|
|
||||||
VALID_ATTRS = ('table', 'only_columns', 'exclude_columns')
|
VALID_ATTRS = ('model', 'only_fields', 'exclude_fields')
|
||||||
|
|
||||||
|
|
||||||
def is_base(cls):
|
def is_base(cls):
|
||||||
from graphene.contrib.SQLAlchemy.types import SQLAlchemyObjectType
|
from graphene.contrib.sqlalchemy.types import SQLAlchemyObjectType
|
||||||
return SQLAlchemyObjectType in cls.__bases__
|
return SQLAlchemyObjectType in cls.__bases__
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyOptions(Options):
|
class SQLAlchemyOptions(Options):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.table = None
|
self.model = None
|
||||||
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
|
super(SQLAlchemyOptions, self).__init__(*args, **kwargs)
|
||||||
self.valid_attrs += VALID_ATTRS
|
self.valid_attrs += VALID_ATTRS
|
||||||
self.only_fields = None
|
self.only_fields = None
|
||||||
|
@ -30,8 +30,8 @@ class SQLAlchemyOptions(Options):
|
||||||
self.interfaces.append(Node)
|
self.interfaces.append(Node)
|
||||||
if not is_node(cls) and not is_base(cls):
|
if not is_node(cls) and not is_base(cls):
|
||||||
return
|
return
|
||||||
if not self.table:
|
if not self.model:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'SQLAlchemy ObjectType %s must have a table in the Meta class attr' % cls)
|
'SQLAlchemy ObjectType %s must have a model in the Meta class attr' % cls)
|
||||||
elif not inspect.isclass(self.table) or not issubclass(self.table, Table):
|
elif not inspect.isclass(self.model) or not isinstance(self.model, DeclarativeMeta):
|
||||||
raise Exception('Provided table in %s is not a SQLAlchemy table' % cls)
|
raise Exception('Provided model in %s is not a SQLAlchemy model' % cls)
|
||||||
|
|
0
graphene/contrib/sqlalchemy/tests/__init__.py
Normal file
0
graphene/contrib/sqlalchemy/tests/__init__.py
Normal file
37
graphene/contrib/sqlalchemy/tests/models.py
Normal file
37
graphene/contrib/sqlalchemy/tests/models.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from sqlalchemy import Table, Column, Integer, String, Date, ForeignKey
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
association_table = Table('association', Base.metadata,
|
||||||
|
Column('pet_id', Integer, ForeignKey('pets.id')),
|
||||||
|
Column('reporter_id', Integer, ForeignKey('reporters.id')))
|
||||||
|
|
||||||
|
|
||||||
|
class Pet(Base):
|
||||||
|
__tablename__ = 'pets'
|
||||||
|
id = Column(Integer(), primary_key=True)
|
||||||
|
name = Column(String(30))
|
||||||
|
reporter_id = Column(Integer(), ForeignKey('reporters.id'))
|
||||||
|
|
||||||
|
|
||||||
|
class Reporter(Base):
|
||||||
|
__tablename__ = 'reporters'
|
||||||
|
id = Column(Integer(), primary_key=True)
|
||||||
|
first_name = Column(String(30))
|
||||||
|
last_name = Column(String(30))
|
||||||
|
email = Column(String())
|
||||||
|
pets = relationship('Pet', secondary=association_table, backref='reporters')
|
||||||
|
articles = relationship('Article', backref='reporter')
|
||||||
|
|
||||||
|
|
||||||
|
class Article(Base):
|
||||||
|
__tablename__ = 'articles'
|
||||||
|
id = Column(Integer(), primary_key=True)
|
||||||
|
headline = Column(String(100))
|
||||||
|
pub_date = Column(Date())
|
||||||
|
reporter_id = Column(Integer(), ForeignKey('reporters.id'))
|
103
graphene/contrib/sqlalchemy/tests/test_converter.py
Normal file
103
graphene/contrib/sqlalchemy/tests/test_converter.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
from sqlalchemy import types, Column
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene.contrib.sqlalchemy.converter import convert_sqlalchemy_column, convert_sqlalchemy_relationship
|
||||||
|
from graphene.contrib.sqlalchemy.fields import ConnectionOrListField, SQLAlchemyModelField
|
||||||
|
|
||||||
|
from .models import Article, Reporter, Pet
|
||||||
|
|
||||||
|
|
||||||
|
def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs):
|
||||||
|
column = Column(sqlalchemy_type, doc='Custom Help Text', **kwargs)
|
||||||
|
graphene_type = convert_sqlalchemy_column(column)
|
||||||
|
assert isinstance(graphene_type, graphene_field)
|
||||||
|
field = graphene_type.as_field()
|
||||||
|
assert field.description == 'Custom Help Text'
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_unknown_sqlalchemy_field_raise_exception():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
convert_sqlalchemy_column(None)
|
||||||
|
assert 'Don\'t know how to convert the SQLAlchemy field' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_date_convert_string():
|
||||||
|
assert_column_conversion(types.Date(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_datetime_convert_string():
|
||||||
|
assert_column_conversion(types.DateTime(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_time_convert_string():
|
||||||
|
assert_column_conversion(types.Time(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_string_convert_string():
|
||||||
|
assert_column_conversion(types.String(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_text_convert_string():
|
||||||
|
assert_column_conversion(types.Text(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_unicode_convert_string():
|
||||||
|
assert_column_conversion(types.Unicode(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_unicodetext_convert_string():
|
||||||
|
assert_column_conversion(types.UnicodeText(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_enum_convert_string():
|
||||||
|
assert_column_conversion(types.Enum(), graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_small_integer_convert_int():
|
||||||
|
assert_column_conversion(types.SmallInteger(), graphene.Int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_big_integer_convert_int():
|
||||||
|
assert_column_conversion(types.BigInteger(), graphene.Int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_integer_convert_int():
|
||||||
|
assert_column_conversion(types.Integer(), graphene.Int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_integer_convert_id():
|
||||||
|
assert_column_conversion(types.Integer(), graphene.ID, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_boolean_convert_boolean():
|
||||||
|
field = assert_column_conversion(types.Boolean(), graphene.Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_float_convert_float():
|
||||||
|
assert_column_conversion(types.Float(), graphene.Float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_numeric_convert_float():
|
||||||
|
assert_column_conversion(types.Numeric(), graphene.Float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_manytomany_convert_connectionorlist():
|
||||||
|
graphene_type = convert_sqlalchemy_relationship(Reporter.pets.property)
|
||||||
|
assert isinstance(graphene_type, ConnectionOrListField)
|
||||||
|
assert isinstance(graphene_type.type, SQLAlchemyModelField)
|
||||||
|
assert graphene_type.type.model == Pet
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_manytoone_convert_connectionorlist():
|
||||||
|
field = convert_sqlalchemy_relationship(Article.reporter.property)
|
||||||
|
assert isinstance(field, SQLAlchemyModelField)
|
||||||
|
assert field.model == Reporter
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_onetomany_convert_model():
|
||||||
|
graphene_type = convert_sqlalchemy_relationship(Reporter.articles.property)
|
||||||
|
assert isinstance(graphene_type, ConnectionOrListField)
|
||||||
|
assert isinstance(graphene_type.type, SQLAlchemyModelField)
|
||||||
|
assert graphene_type.type.model == Article
|
141
graphene/contrib/sqlalchemy/tests/test_query.py
Normal file
141
graphene/contrib/sqlalchemy/tests/test_query.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyObjectType
|
||||||
|
from .models import Article, Reporter
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_query_only_fields():
|
||||||
|
with raises(Exception):
|
||||||
|
class ReporterType(SQLAlchemyObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
only_fields = ('articles', )
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=ReporterType)
|
||||||
|
query = '''
|
||||||
|
query ReporterQuery {
|
||||||
|
articles
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_query_well():
|
||||||
|
class ReporterType(SQLAlchemyObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
reporter = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def resolve_reporter(self, *args, **kwargs):
|
||||||
|
return ReporterType(Reporter(first_name='ABA', last_name='X'))
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query ReporterQuery {
|
||||||
|
reporter {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'reporter': {
|
||||||
|
'firstName': 'ABA',
|
||||||
|
'lastName': 'X',
|
||||||
|
'email': None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_node():
|
||||||
|
class ReporterNode(SQLAlchemyNode):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
exclude_fields = ('id', )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id, info):
|
||||||
|
return ReporterNode(Reporter(id=2, first_name='Cookie Monster'))
|
||||||
|
|
||||||
|
def resolve_articles(self, *args, **kwargs):
|
||||||
|
return [ArticleNode(Article(headline='Hi!'))]
|
||||||
|
|
||||||
|
class ArticleNode(SQLAlchemyNode):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
exclude_fields = ('id', )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, id, info):
|
||||||
|
return ArticleNode(Article(id=1, headline='Article node'))
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
node = relay.NodeField()
|
||||||
|
reporter = graphene.Field(ReporterNode)
|
||||||
|
article = graphene.Field(ArticleNode)
|
||||||
|
|
||||||
|
def resolve_reporter(self, *args, **kwargs):
|
||||||
|
return ReporterNode(Reporter(id=1, first_name='ABA', last_name='X'))
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query ReporterQuery {
|
||||||
|
reporter {
|
||||||
|
id,
|
||||||
|
firstName,
|
||||||
|
articles {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
headline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastName,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") {
|
||||||
|
id
|
||||||
|
... on ReporterNode {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
... on ArticleNode {
|
||||||
|
headline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'reporter': {
|
||||||
|
'id': 'UmVwb3J0ZXJOb2RlOjE=',
|
||||||
|
'firstName': 'ABA',
|
||||||
|
'lastName': 'X',
|
||||||
|
'email': None,
|
||||||
|
'articles': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'headline': 'Hi!'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'myArticle': {
|
||||||
|
'id': 'QXJ0aWNsZU5vZGU6MQ==',
|
||||||
|
'headline': 'Article node'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
45
graphene/contrib/sqlalchemy/tests/test_schema.py
Normal file
45
graphene/contrib/sqlalchemy/tests/test_schema.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from py.test import raises
|
||||||
|
|
||||||
|
from graphene.contrib.sqlalchemy import SQLAlchemyObjectType
|
||||||
|
from tests.utils import assert_equal_lists
|
||||||
|
|
||||||
|
from .models import Reporter
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_raise_if_no_model():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
class Character1(SQLAlchemyObjectType):
|
||||||
|
pass
|
||||||
|
assert 'model in the Meta' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_raise_if_model_is_invalid():
|
||||||
|
with raises(Exception) as excinfo:
|
||||||
|
class Character2(SQLAlchemyObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = 1
|
||||||
|
assert 'not a SQLAlchemy model' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_map_fields_correctly():
|
||||||
|
class ReporterType2(SQLAlchemyObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
assert_equal_lists(
|
||||||
|
ReporterType2._meta.fields_map.keys(),
|
||||||
|
['articles', 'first_name', 'last_name', 'email', 'pets', 'id']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_map_only_few_fields():
|
||||||
|
class Reporter2(SQLAlchemyObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
only_fields = ('id', 'email')
|
||||||
|
assert_equal_lists(
|
||||||
|
Reporter2._meta.fields_map.keys(),
|
||||||
|
['id', 'email']
|
||||||
|
)
|
106
graphene/contrib/sqlalchemy/tests/test_types.py
Normal file
106
graphene/contrib/sqlalchemy/tests/test_types.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
from graphql.core.type import GraphQLInterfaceType, GraphQLObjectType
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from graphene import Schema
|
||||||
|
from graphene.contrib.sqlalchemy.types import SQLAlchemyInterface, SQLAlchemyNode
|
||||||
|
from graphene.core.fields import Field
|
||||||
|
from graphene.core.types.scalars import Int
|
||||||
|
from graphene.relay.fields import GlobalIDField
|
||||||
|
from tests.utils import assert_equal_lists
|
||||||
|
|
||||||
|
from .models import Article, Reporter
|
||||||
|
|
||||||
|
schema = Schema()
|
||||||
|
|
||||||
|
|
||||||
|
class Character(SQLAlchemyInterface):
|
||||||
|
'''Character description'''
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
|
||||||
|
|
||||||
|
@schema.register
|
||||||
|
class Human(SQLAlchemyNode):
|
||||||
|
'''Human description'''
|
||||||
|
|
||||||
|
pub_date = Int()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
exclude_fields = ('id', )
|
||||||
|
|
||||||
|
|
||||||
|
def test_sqlalchemy_interface():
|
||||||
|
assert SQLAlchemyNode._meta.is_interface is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_sqlalchemy_get_node(get):
|
||||||
|
human = Human.get_node(1, None)
|
||||||
|
get.assert_called_with(id=1)
|
||||||
|
assert human.id == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_pseudo_interface_registered():
|
||||||
|
object_type = schema.T(Character)
|
||||||
|
assert Character._meta.is_interface is True
|
||||||
|
assert isinstance(object_type, GraphQLInterfaceType)
|
||||||
|
assert Character._meta.model == Reporter
|
||||||
|
assert_equal_lists(
|
||||||
|
object_type.get_fields().keys(),
|
||||||
|
['articles', 'firstName', 'lastName', 'email', 'pets', 'id']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sqlalchemynode_idfield():
|
||||||
|
idfield = SQLAlchemyNode._meta.fields_map['id']
|
||||||
|
assert isinstance(idfield, GlobalIDField)
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_idfield():
|
||||||
|
idfield = Human._meta.fields_map['id']
|
||||||
|
assert isinstance(idfield, GlobalIDField)
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_replacedfield():
|
||||||
|
idfield = Human._meta.fields_map['pub_date']
|
||||||
|
assert isinstance(idfield, Field)
|
||||||
|
assert schema.T(idfield).type == schema.T(Int())
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_resolve_type():
|
||||||
|
resolve_type = Character.resolve_type(schema, Human())
|
||||||
|
assert isinstance(resolve_type, GraphQLObjectType)
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_objecttype_init_none():
|
||||||
|
h = Human()
|
||||||
|
assert h._root is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_objecttype_init_good():
|
||||||
|
instance = Article()
|
||||||
|
h = Human(instance)
|
||||||
|
assert h._root == instance
|
||||||
|
|
||||||
|
|
||||||
|
def test_interface_objecttype_init_unexpected():
|
||||||
|
with raises(AssertionError) as excinfo:
|
||||||
|
Human(object())
|
||||||
|
assert str(excinfo.value) == "Human received a non-compatible instance (object) when expecting Article"
|
||||||
|
|
||||||
|
|
||||||
|
def test_object_type():
|
||||||
|
object_type = schema.T(Human)
|
||||||
|
Human._meta.fields_map
|
||||||
|
assert Human._meta.is_interface is False
|
||||||
|
assert isinstance(object_type, GraphQLObjectType)
|
||||||
|
assert_equal_lists(
|
||||||
|
object_type.get_fields().keys(),
|
||||||
|
['headline', 'id', 'reporter', 'reporterId', 'pubDate']
|
||||||
|
)
|
||||||
|
assert schema.T(SQLAlchemyNode) in object_type.get_interfaces()
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_notinterface():
|
||||||
|
assert Human._meta.is_interface is False
|
||||||
|
assert SQLAlchemyNode in Human._meta.interfaces
|
|
@ -1,13 +1,11 @@
|
||||||
import six
|
import six
|
||||||
from sqlalchemy.inspection import inspect
|
from sqlalchemy.inspection import inspect
|
||||||
|
|
||||||
from graphene.contrib.sqlalchemy.converter import convert_sqlalchemy_column,
|
from ...core.types import BaseObjectType, ObjectTypeMeta
|
||||||
convert_sqlalchemy_relationship
|
from ...relay.fields import GlobalIDField
|
||||||
from graphene.contrib.sqlalchemy.options import SQLAlchemyOptions
|
from ...relay.types import BaseNode
|
||||||
from graphene.contrib.sqlalchemy.utils import get_reverse_columns
|
from .converter import convert_sqlalchemy_column, convert_sqlalchemy_relationship
|
||||||
from graphene.core.types import BaseObjectType, ObjectTypeMeta
|
from .options import SQLAlchemyOptions
|
||||||
from graphene.relay.fields import GlobalIDField
|
|
||||||
from graphene.relay.types import BaseNode
|
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
|
class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
|
||||||
|
@ -17,26 +15,60 @@ class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
|
||||||
return SQLAlchemyInterface in parents
|
return SQLAlchemyInterface in parents
|
||||||
|
|
||||||
def add_extra_fields(cls):
|
def add_extra_fields(cls):
|
||||||
if not cls._meta.table:
|
if not cls._meta.model:
|
||||||
return
|
return
|
||||||
inspected_table = inspect(cls._meta.table)
|
only_fields = cls._meta.only_fields
|
||||||
# Get all the columns for the relationships on the table
|
exclude_fields = cls._meta.exclude_fields
|
||||||
for relationship in inspected_table.relationships:
|
already_created_fields = {f.attname for f in cls._meta.local_fields}
|
||||||
|
inspected_model = inspect(cls._meta.model)
|
||||||
|
|
||||||
|
# Get all the columns for the relationships on the model
|
||||||
|
for relationship in inspected_model.relationships:
|
||||||
|
is_not_in_only = only_fields and relationship.key not in only_fields
|
||||||
|
is_already_created = relationship.key in already_created_fields
|
||||||
|
is_excluded = relationship.key in exclude_fields or is_already_created
|
||||||
|
if is_not_in_only or is_excluded:
|
||||||
|
# We skip this field if we specify only_fields and is not
|
||||||
|
# in there. Or when we excldue this field in exclude_fields
|
||||||
|
continue
|
||||||
converted_relationship = convert_sqlalchemy_relationship(relationship)
|
converted_relationship = convert_sqlalchemy_relationship(relationship)
|
||||||
cls.add_to_class(relationship.key, converted_relationship)
|
cls.add_to_class(relationship.key, converted_relationship)
|
||||||
for column in inspected_table.columns:
|
|
||||||
|
for column in inspected_model.columns:
|
||||||
|
is_not_in_only = only_fields and column.name not in only_fields
|
||||||
|
is_already_created = column.name in already_created_fields
|
||||||
|
is_excluded = column.name in exclude_fields or is_already_created
|
||||||
|
if is_not_in_only or is_excluded:
|
||||||
|
# We skip this field if we specify only_fields and is not
|
||||||
|
# in there. Or when we excldue this field in exclude_fields
|
||||||
|
continue
|
||||||
converted_column = convert_sqlalchemy_column(column)
|
converted_column = convert_sqlalchemy_column(column)
|
||||||
cls.add_to_class(column.name, converted_column)
|
cls.add_to_class(column.name, converted_column)
|
||||||
|
|
||||||
|
|
||||||
class InstanceObjectType(BaseObjectType):
|
class InstanceObjectType(BaseObjectType):
|
||||||
|
|
||||||
def __init__(self, instance=None):
|
def __init__(self, _root=None):
|
||||||
self.instance = instance
|
if _root:
|
||||||
super(InstanceObjectType, self).__init__()
|
assert isinstance(_root, self._meta.model), (
|
||||||
|
'{} received a non-compatible instance ({}) '
|
||||||
|
'when expecting {}'.format(
|
||||||
|
self.__class__.__name__,
|
||||||
|
_root.__class__.__name__,
|
||||||
|
self._meta.model.__name__
|
||||||
|
))
|
||||||
|
super(InstanceObjectType, self).__init__(_root=_root)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance(self):
|
||||||
|
return self._root
|
||||||
|
|
||||||
|
@instance.setter
|
||||||
|
def instance(self, value):
|
||||||
|
self._root = value
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.instance, attr)
|
return getattr(self._root, attr)
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, InstanceObjectType)):
|
class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, InstanceObjectType)):
|
||||||
|
@ -51,6 +83,9 @@ class SQLAlchemyNode(BaseNode, SQLAlchemyInterface):
|
||||||
id = GlobalIDField()
|
id = GlobalIDField()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node(cls, id):
|
def get_node(cls, id, info=None):
|
||||||
instance = cls._meta.table.objects.filter(id=id).first()
|
try:
|
||||||
return cls(instance)
|
instance = cls._meta.model.filter(id=id).one()
|
||||||
|
return cls(instance)
|
||||||
|
except cls._meta.model.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
from django.db import models
|
|
||||||
from django.db.models.manager import Manager
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
|
|
||||||
from graphene.utils import LazyMap
|
|
||||||
|
|
||||||
|
|
||||||
def get_type_for_model(schema, model):
|
def get_type_for_model(schema, model):
|
||||||
schema = schema
|
schema = schema
|
||||||
types = schema.types.values()
|
types = schema.types.values()
|
||||||
|
@ -13,11 +6,3 @@ def get_type_for_model(schema, model):
|
||||||
_type._meta, 'model', None)
|
_type._meta, 'model', None)
|
||||||
if model == type_model:
|
if model == type_model:
|
||||||
return _type
|
return _type
|
||||||
|
|
||||||
|
|
||||||
def lazy_map(value, func):
|
|
||||||
if isinstance(value, Manager):
|
|
||||||
value = value.get_queryset()
|
|
||||||
if isinstance(value, QuerySet):
|
|
||||||
return LazyMap(value, func)
|
|
||||||
return value
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user