Skip to content

Commit a9465fc

Browse files
authored
SQLAlchemy JSON support (#40)
1 parent 8ffa1f4 commit a9465fc

File tree

5 files changed

+47
-9
lines changed

5 files changed

+47
-9
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
repos:
22
- repo: https://github.com/psf/black
3-
rev: 23.7.0
3+
rev: 23.9.1
44
hooks:
55
- id: black
66
exclude: ^tests/\w+/snapshots/
77

88
- repo: https://github.com/astral-sh/ruff-pre-commit
9-
rev: v0.0.280
9+
rev: v0.0.289
1010
hooks:
1111
- id: ruff
1212
exclude: ^tests/\w+/snapshots/
@@ -18,7 +18,7 @@ repos:
1818
exclude: (CHANGELOG|TWEET).md
1919

2020
- repo: https://github.com/pre-commit/mirrors-prettier
21-
rev: v3.0.0
21+
rev: v3.0.3
2222
hooks:
2323
- id: prettier
2424
files: '^docs/.*\.mdx?$'
@@ -33,7 +33,7 @@ repos:
3333
- id: check-toml
3434

3535
- repo: https://github.com/adamchainz/blacken-docs
36-
rev: 1.15.0
36+
rev: 1.16.0
3737
hooks:
3838
- id: blacken-docs
3939
args: [--skip-errors]

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,21 @@ from sqlalchemy.ext.declarative import declarative_base
3232

3333
Base = declarative_base()
3434

35+
3536
class Employee(Base):
36-
__tablename__ = 'employee'
37+
__tablename__ = "employee"
3738
id = Column(UUID, primary_key=True)
3839
name = Column(String, nullable=False)
3940
password_hash = Column(String, nullable=False)
40-
department_id = Column(UUID, ForeignKey('department.id'))
41-
department = relationship('Department', back_populates='employees')
41+
department_id = Column(UUID, ForeignKey("department.id"))
42+
department = relationship("Department", back_populates="employees")
43+
4244

4345
class Department(Base):
4446
__tablename__ = "department"
4547
id = Column(UUID, primary_key=True)
4648
name = Column(String, nullable=False)
47-
employees = relationship('Employee', back_populates='department')
49+
employees = relationship("Employee", back_populates="department")
4850
```
4951

5052
Next, decorate a type with `strawberry_sqlalchemy_mapper.type()`
@@ -58,6 +60,8 @@ and hybrid properties. For example:
5860
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
5961

6062
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
63+
64+
6165
@strawberry_sqlalchemy_mapper.type(models.Employee)
6266
class Employee:
6367
__exclude__ = ["password_hash"]
@@ -67,6 +71,7 @@ class Employee:
6771
class Department:
6872
pass
6973

74+
7075
@strawberry.type
7176
class Query:
7277
@strawberry.field
@@ -81,6 +86,7 @@ class CustomGraphQLView(GraphQLView):
8186
"sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=YOUR_SESSION),
8287
}
8388

89+
8490
# call finalize() before using the schema:
8591
# (note that models that are related to models that are in the schema
8692
# are automatically mapped at this stage -- e.g., Department is mapped
@@ -144,6 +150,7 @@ SmallInteger: int,
144150
SQLAlchemyUUID: uuid.UUID,
145151
VARCHAR: str,
146152
ARRAY[T]: List[T] # PostgreSQL array
153+
JSON: JSON # SQLAlchemy JSON
147154
Enum: (the Python enum it is mapped to, which should be @strawberry.enum-decorated)
148155
```
149156

@@ -161,28 +168,34 @@ its descendants are expected to inherit from the interface:
161168
class Book(Model):
162169
id = Column(UUID, primary_key=True)
163170

171+
164172
class Novel(Book):
165173
pass
166174

175+
167176
class ShortStory(Book):
168177
pass
169178

170179

171180
# in another file
172181
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
173182

183+
174184
@strawberry_sqlalchemy_mapper.interface(models.Book)
175185
class BookInterface:
176186
pass
177187

188+
178189
@strawberry_sqlalchemy_mapper.type(models.Book)
179190
class Book:
180191
pass
181192

193+
182194
@strawberry_sqlalchemy_mapper.type(models.Novel)
183195
class Novel:
184196
pass
185197

198+
186199
@strawberry_sqlalchemy_mapper.type(models.ShortStory)
187200
class ShortStory:
188201
pass

RELEASE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Release type: minor
2+
3+
Native SQLAlchemy JSON Conversion Support. Added native support for SQLAlchemy JSON conversions. Now, you'll find that `sqlalchemy.JSON` is converted to `strawberry.scalars.JSON` for enhanced compatibility.

src/strawberry_sqlalchemy_mapper/mapper.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import strawberry
3333
from sqlalchemy import (
3434
ARRAY,
35+
JSON,
3536
VARCHAR,
3637
BigInteger,
3738
Boolean,
@@ -65,6 +66,7 @@
6566
from sqlalchemy.sql.type_api import TypeEngine
6667
from strawberry.annotation import StrawberryAnnotation
6768
from strawberry.field import StrawberryField
69+
from strawberry.scalars import JSON as StrawberryJSON
6870
from strawberry.types import Info
6971

7072
from strawberry_sqlalchemy_mapper.exc import (
@@ -131,6 +133,7 @@ class StrawberrySQLAlchemyMapper(Generic[BaseModelType]):
131133
SmallInteger: int,
132134
SQLAlchemyUUID: uuid.UUID,
133135
VARCHAR: str,
136+
JSON: StrawberryJSON,
134137
}
135138
#: Mapping from sqlalchemy types to strawberry types
136139
sqlalchemy_type_to_strawberry_type_map: MutableMapping[

tests/test_mapper.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from typing import List, Optional
33

44
import pytest
5-
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
5+
from sqlalchemy import JSON, Column, Enum, ForeignKey, Integer, String
66
from sqlalchemy.dialects.postgresql.array import ARRAY
77
from sqlalchemy.orm import relationship
8+
from strawberry.scalars import JSON as StrawberryJSON
89
from strawberry.type import StrawberryOptional
910
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
1011

@@ -133,13 +134,31 @@ class ParaLegal(Lawyer):
133134
assert mapper._get_polymorphic_base_model(ParaLegal) == Employee
134135

135136

137+
def test_convert_all_columns_to_strawberry_type(mapper):
138+
for (
139+
sqlalchemy_type,
140+
strawberry_type,
141+
) in mapper.sqlalchemy_type_to_strawberry_type_map.items():
142+
assert (
143+
mapper._convert_column_to_strawberry_type(
144+
Column(sqlalchemy_type, nullable=False)
145+
)
146+
== strawberry_type
147+
)
148+
149+
136150
def test_convert_column_to_strawberry_type(mapper):
137151
int_column = Column(Integer, nullable=False)
138152
assert mapper._convert_column_to_strawberry_type(int_column) == int
139153
string_column = Column(String, nullable=False)
140154
assert mapper._convert_column_to_strawberry_type(string_column) == str
141155

142156

157+
def test_convert_json_column_to_strawberry_type(mapper):
158+
json_colum = Column(JSON, nullable=False)
159+
assert mapper._convert_column_to_strawberry_type(json_colum) == StrawberryJSON
160+
161+
143162
def test_convert_array_column_to_strawberry_type(mapper):
144163
column = Column(ARRAY(String))
145164
assert mapper._convert_column_to_strawberry_type(column) == Optional[List[str]]

0 commit comments

Comments
 (0)