db-graphql-gateway 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. db_graphql_gateway-0.1.0/.github/workflows/ci.yml +32 -0
  2. db_graphql_gateway-0.1.0/.github/workflows/conformance.yml +33 -0
  3. db_graphql_gateway-0.1.0/.github/workflows/docs.yml +31 -0
  4. db_graphql_gateway-0.1.0/.github/workflows/integration.yml +30 -0
  5. db_graphql_gateway-0.1.0/.gitignore +57 -0
  6. db_graphql_gateway-0.1.0/.pre-commit-config.yaml +12 -0
  7. db_graphql_gateway-0.1.0/CONTRIBUTING_ADAPTER.md +94 -0
  8. db_graphql_gateway-0.1.0/LICENSE +21 -0
  9. db_graphql_gateway-0.1.0/Makefile +15 -0
  10. db_graphql_gateway-0.1.0/PKG-INFO +48 -0
  11. db_graphql_gateway-0.1.0/README.md +13 -0
  12. db_graphql_gateway-0.1.0/STATE.md +27 -0
  13. db_graphql_gateway-0.1.0/docs/ARCHITECTURE.md +229 -0
  14. db_graphql_gateway-0.1.0/docs/BENCHMARKS.md +38 -0
  15. db_graphql_gateway-0.1.0/docs/CHANGELOG.md +30 -0
  16. db_graphql_gateway-0.1.0/docs/CONTRIBUTING_ADAPTER.md +108 -0
  17. db_graphql_gateway-0.1.0/docs/FAQ.md +62 -0
  18. db_graphql_gateway-0.1.0/docs/SECURITY.md +74 -0
  19. db_graphql_gateway-0.1.0/docs/cli.md +127 -0
  20. db_graphql_gateway-0.1.0/docs/index.md +133 -0
  21. db_graphql_gateway-0.1.0/docs/quickstart.md +167 -0
  22. db_graphql_gateway-0.1.0/docs/stylesheets/custom.css +58 -0
  23. db_graphql_gateway-0.1.0/integration_tests/docker-compose.override.yml +14 -0
  24. db_graphql_gateway-0.1.0/integration_tests/docker-compose.yml +22 -0
  25. db_graphql_gateway-0.1.0/integration_tests/engine.py +65 -0
  26. db_graphql_gateway-0.1.0/integration_tests/level1_basic.py +61 -0
  27. db_graphql_gateway-0.1.0/integration_tests/level2_medium.py +99 -0
  28. db_graphql_gateway-0.1.0/integration_tests/level3_advanced.py +132 -0
  29. db_graphql_gateway-0.1.0/integration_tests/run_all.sh +33 -0
  30. db_graphql_gateway-0.1.0/integration_tests/run_integration.py +67 -0
  31. db_graphql_gateway-0.1.0/integration_tests/schema.sql +53 -0
  32. db_graphql_gateway-0.1.0/integration_tests/seed.py +102 -0
  33. db_graphql_gateway-0.1.0/integration_tests/server.py +101 -0
  34. db_graphql_gateway-0.1.0/integration_tests/sgql.yaml +25 -0
  35. db_graphql_gateway-0.1.0/mkdocs.yml +95 -0
  36. db_graphql_gateway-0.1.0/pyproject.toml +100 -0
  37. db_graphql_gateway-0.1.0/sgql.yaml +11 -0
  38. db_graphql_gateway-0.1.0/src/db_graphql_gateway/__init__.py +8 -0
  39. db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/__init__.py +10 -0
  40. db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/authorization.py +62 -0
  41. db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/interfaces.py +15 -0
  42. db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/jwt_provider.py +78 -0
  43. db_graphql_gateway-0.1.0/src/db_graphql_gateway/auth/middleware.py +10 -0
  44. db_graphql_gateway-0.1.0/src/db_graphql_gateway/cli/__init__.py +1 -0
  45. db_graphql_gateway-0.1.0/src/db_graphql_gateway/cli/main.py +241 -0
  46. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/__init__.py +1 -0
  47. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/__init__.py +1 -0
  48. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/base_compiler.py +285 -0
  49. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/interfaces.py +135 -0
  50. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/__init__.py +1 -0
  51. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/adapter.py +216 -0
  52. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/compiler.py +24 -0
  53. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/inspector.py +177 -0
  54. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/mysql/mapper.py +61 -0
  55. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/__init__.py +1 -0
  56. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/adapter.py +85 -0
  57. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/compiler.py +22 -0
  58. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/inspector.py +198 -0
  59. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/postgres/mapper.py +27 -0
  60. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/__init__.py +1 -0
  61. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/adapter.py +186 -0
  62. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/compiler.py +26 -0
  63. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/inspector.py +257 -0
  64. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/adapters/sqlite/mapper.py +38 -0
  65. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/models/__init__.py +1 -0
  66. db_graphql_gateway-0.1.0/src/db_graphql_gateway/database/models/schema.py +72 -0
  67. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/__init__.py +1 -0
  68. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/builder.py +588 -0
  69. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/dataloader.py +95 -0
  70. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/filter_builder.py +168 -0
  71. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/mutation_builder.py +49 -0
  72. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/pagination.py +41 -0
  73. db_graphql_gateway-0.1.0/src/db_graphql_gateway/graphql/query_planner.py +33 -0
  74. db_graphql_gateway-0.1.0/src/db_graphql_gateway/integrations/fastapi_integration.py +48 -0
  75. db_graphql_gateway-0.1.0/src/db_graphql_gateway/integrations/sqlalchemy_integration.py +58 -0
  76. db_graphql_gateway-0.1.0/src/db_graphql_gateway/py.typed +0 -0
  77. db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/__init__.py +1 -0
  78. db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/config.py +19 -0
  79. db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/ir/__init__.py +1 -0
  80. db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/ir/builder.py +160 -0
  81. db_graphql_gateway-0.1.0/src/db_graphql_gateway/schema/ir/models.py +76 -0
  82. db_graphql_gateway-0.1.0/src/db_graphql_gateway/security/complexity.py +60 -0
  83. db_graphql_gateway-0.1.0/src/db_graphql_gateway/security/error_masking.py +15 -0
  84. db_graphql_gateway-0.1.0/src/db_graphql_gateway/security/validation.py +101 -0
  85. db_graphql_gateway-0.1.0/src/db_graphql_gateway/version.py +1 -0
  86. db_graphql_gateway-0.1.0/tests/__init__.py +0 -0
  87. db_graphql_gateway-0.1.0/tests/conformance/conftest.py +212 -0
  88. db_graphql_gateway-0.1.0/tests/conformance/test_authorization.py +76 -0
  89. db_graphql_gateway-0.1.0/tests/conformance/test_mutations.py +85 -0
  90. db_graphql_gateway-0.1.0/tests/conformance/test_queries.py +56 -0
  91. db_graphql_gateway-0.1.0/tests/conformance/test_relationships.py +56 -0
  92. db_graphql_gateway-0.1.0/tests/integration/__init__.py +0 -0
  93. db_graphql_gateway-0.1.0/tests/integration/conftest.py +49 -0
  94. db_graphql_gateway-0.1.0/tests/integration/test_authorization.py +126 -0
  95. db_graphql_gateway-0.1.0/tests/integration/test_filtering_pagination.py +198 -0
  96. db_graphql_gateway-0.1.0/tests/integration/test_final_acceptance.py +148 -0
  97. db_graphql_gateway-0.1.0/tests/integration/test_graphql_execution.py +55 -0
  98. db_graphql_gateway-0.1.0/tests/integration/test_mutations.py +145 -0
  99. db_graphql_gateway-0.1.0/tests/integration/test_mysql_integration.py +281 -0
  100. db_graphql_gateway-0.1.0/tests/integration/test_postgres_introspection.py +47 -0
  101. db_graphql_gateway-0.1.0/tests/integration/test_query_planner.py +100 -0
  102. db_graphql_gateway-0.1.0/tests/integration/test_relationships_dataloader.py +131 -0
  103. db_graphql_gateway-0.1.0/tests/integration/test_sqlite_integration.py +300 -0
  104. db_graphql_gateway-0.1.0/tests/unit/__init__.py +0 -0
  105. db_graphql_gateway-0.1.0/tests/unit/test_authentication.py +204 -0
  106. db_graphql_gateway-0.1.0/tests/unit/test_base_compiler.py +343 -0
  107. db_graphql_gateway-0.1.0/tests/unit/test_cli.py +77 -0
  108. db_graphql_gateway-0.1.0/tests/unit/test_integrations.py +52 -0
  109. db_graphql_gateway-0.1.0/tests/unit/test_interfaces.py +11 -0
  110. db_graphql_gateway-0.1.0/tests/unit/test_ir_builder.py +143 -0
  111. db_graphql_gateway-0.1.0/tests/unit/test_mysql_compiler.py +228 -0
  112. db_graphql_gateway-0.1.0/tests/unit/test_security_hardening.py +180 -0
  113. db_graphql_gateway-0.1.0/tests/unit/test_sqlite_adapter.py +386 -0
  114. db_graphql_gateway-0.1.0/uv.lock +1940 -0
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version-file: "pyproject.toml"
24
+
25
+ - name: Install dependencies
26
+ run: uv sync --all-extras --dev
27
+
28
+ - name: Run pre-commit
29
+ run: uv run pre-commit run --all-files
30
+
31
+ - name: Run tests
32
+ run: uv run pytest tests/ -v
@@ -0,0 +1,33 @@
1
+ name: Conformance Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ conformance:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ adapter: [postgres, mysql, sqlite]
15
+ fail-fast: false
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v2
22
+ with:
23
+ enable-cache: true
24
+ cache-dependency-glob: "pyproject.toml"
25
+
26
+ - name: Set up Python
27
+ run: uv python install 3.12
28
+
29
+ - name: Install Dependencies
30
+ run: uv sync --all-extras --dev
31
+
32
+ - name: Run Conformance Suite
33
+ run: uv run pytest tests/conformance/ -v -k "[${{ matrix.adapter }}]"
@@ -0,0 +1,31 @@
1
+ name: Deploy docs
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Configure Git Credentials
18
+ run: |
19
+ git config user.name github-actions[bot]
20
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
21
+
22
+ - name: Setup Python
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: '3.12'
26
+
27
+ - name: Install MkDocs Material
28
+ run: pip install mkdocs-material
29
+
30
+ - name: Deploy Docs to GitHub Pages
31
+ run: mkdocs gh-deploy --force
@@ -0,0 +1,30 @@
1
+ name: Integration Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ integration-test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version-file: "pyproject.toml"
24
+
25
+ - name: Install dependencies
26
+ run: uv sync --all-extras --dev
27
+
28
+ - name: Run Integration Tests
29
+ run: |
30
+ ./integration_tests/run_all.sh
@@ -0,0 +1,57 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # Virtual environments
30
+ venv/
31
+ .venv/
32
+ env/
33
+ .env/
34
+ ENV/
35
+
36
+ # IDEs
37
+ .idea/
38
+ .vscode/
39
+ *.swp
40
+
41
+ # Testing and Linting
42
+ .mypy_cache/
43
+ .ruff_cache/
44
+ .pytest_cache/
45
+ .coverage
46
+ htmlcov/
47
+ .tox/
48
+
49
+ # MkDocs
50
+ site*
51
+
52
+ # Environment Variables
53
+ .env
54
+
55
+ # OS generated files
56
+ .DS_Store
57
+ Thumbs.db
@@ -0,0 +1,12 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.4.4
4
+ hooks:
5
+ - id: ruff
6
+ args: [ --fix ]
7
+ - id: ruff-format
8
+ - repo: https://github.com/pre-commit/mirrors-mypy
9
+ rev: v1.10.0
10
+ hooks:
11
+ - id: mypy
12
+ additional_dependencies: ["types-PyYAML", "strawberry-graphql", "PyJWT", "testcontainers"]
@@ -0,0 +1,94 @@
1
+ # Contributing a New Database Adapter
2
+
3
+ Thank you for your interest in expanding the database support of `db-graphql-gateway`! Our goal is to maintain a truly database-agnostic GraphQL interface. This means that users should be able to swap adapters without modifying their GraphQL schemas, authorization policies, or application code.
4
+
5
+ This guide explains the `DatabaseAdapter` protocol, how to build a new adapter, and how to prove it works using the **Cross-Adapter Conformance Suite**.
6
+
7
+ ## The `DatabaseAdapter` Protocol
8
+
9
+ To integrate a new database engine (e.g., SQL Server, DuckDB, CockroachDB), you must implement the `DatabaseAdapter` interface (found in `src/db_graphql_gateway/database/adapters/interfaces.py`).
10
+
11
+ The adapter acts as a bridge between the engine-agnostic core (`GraphQLSchemaBuilder`, `AuthorizationEngine`) and the underlying database dialect.
12
+
13
+ ### Key Components
14
+
15
+ 1. **`DatabaseAdapter`**: Manages the connection pool, executes queries, and returns raw data.
16
+ 2. **`SchemaInspector`**: Introspects the database to dynamically generate the canonical schema.
17
+ 3. **`TypeMapper`**: Translates dialect-specific types (e.g., MySQL's `TINYINT(1)`) into abstract GraphQL IR types (`GraphQLType.BOOLEAN`).
18
+ 4. **`QueryCompiler`**: Usually inherits from `BaseQueryCompiler` and overrides dialect-specific AST-to-SQL logic.
19
+
20
+ ### Capability Flags
21
+
22
+ When configuring your `QueryCompiler` or `Adapter`, you may need to define capability flags that instruct the core gateway on how to handle the dialect's quirks:
23
+
24
+ - `supports_returning` (bool): Set to `True` if your engine supports `RETURNING` clauses (like Postgres). If `False` (like SQLite/MySQL), the gateway will automatically use a "SELECT-after-write" pattern.
25
+ - `fetch_after_write` (bool): Flag emitted in the `MutationPlan` for engines lacking `RETURNING` to trigger the secondary select.
26
+ - `placeholder_style` (enum/string): e.g., `?` for SQLite, `%s` for MySQL, `$1` for Postgres. Ensures parameterized queries match the underlying driver's expectations.
27
+
28
+ ## Building a New Adapter: Worked Example (SQLite)
29
+
30
+ Let's look at how SQLite was implemented.
31
+
32
+ ### 1. Connection & Execution
33
+ ```python
34
+ class SQLiteAdapter(DatabaseAdapter):
35
+ async def execute_query(self, query: str, params: list[Any]) -> list[dict[str, Any]]:
36
+ async with self.pool.acquire() as conn:
37
+ cursor = await conn.execute(query, params)
38
+ rows = await cursor.fetchall()
39
+ return [dict(row) for row in rows]
40
+ ```
41
+
42
+ ### 2. Query Compilation
43
+ Inherit from `BaseQueryCompiler` to get 90% of ANSI SQL behavior for free.
44
+ ```python
45
+ class SQLiteQueryCompiler(BaseQueryCompiler):
46
+ def __init__(self):
47
+ super().__init__()
48
+ self.placeholder = "?"
49
+ self.supports_returning = False
50
+ ```
51
+
52
+ ### 3. Engine Gotchas (Checklist)
53
+
54
+ Before building, verify the following against your driver:
55
+ - [ ] **Placeholder Style**: Does the driver use `?`, `%s`, `:name`, or `$1`?
56
+ - [ ] **Identifier Quoting**: Does it use `""` (Postgres/SQLite) or `\`\`` (MySQL)? Override `quote_identifier()` in your compiler.
57
+ - [ ] **Boolean Types**: Does the engine lack a native `BOOLEAN`? Ensure your `TypeMapper` intercepts it (e.g., `TINYINT(1) -> BOOLEAN`).
58
+ - [ ] **Upsert Syntax**: Does it use `ON CONFLICT` or `ON DUPLICATE KEY UPDATE`?
59
+
60
+ ### Explicit Non-Goals
61
+ The `DatabaseAdapter` protocol does **not** need to support highly proprietary, engine-specific extensions (e.g., Postgres PostGIS, Oracle XMLType) if there is no cross-engine equivalent. Expose these via adapter-level opt-in configuration, not by muddying the core protocol.
62
+
63
+ ## Passing the Conformance Suite
64
+
65
+ Your PR **will not be merged** unless it passes the Conformance Suite. This suite runs identical GraphQL queries against all adapters to guarantee 100% behavioral parity (including pagination, relationships, and authorization pushdowns).
66
+
67
+ ### How to hook into the Conformance Suite:
68
+
69
+ 1. Open `tests/conformance/conftest.py`.
70
+ 2. Add your engine to the parameter matrix:
71
+ ```python
72
+ @pytest_asyncio.fixture(params=["sqlite", "mysql", "postgres", "your_engine"])
73
+ ```
74
+ 3. Update the `db_adapter` fixture to provision your engine (use `testcontainers` if it requires a daemon). The fixture must use the generic `get_ddl("your_engine")` string to instantiate the canonical schema:
75
+ ```python
76
+ elif engine == "your_engine":
77
+ # Setup your engine here
78
+ for stmt in get_ddl("your_engine"):
79
+ await conn.execute(stmt)
80
+ ```
81
+ 4. Run the suite: `uv run pytest tests/conformance/ -k "db_adapter[your_engine]"`
82
+
83
+ If your compiler and type mapper are correct, all tests will pass without writing a single line of test code!
84
+
85
+ ## Entry Points & Packaging
86
+
87
+ To keep the core package lightweight, third-party adapters should be published as separate packages (e.g., `db-graphql-gateway-duckdb`).
88
+
89
+ Register your adapter in your `pyproject.toml` so the gateway can auto-discover it:
90
+ ```toml
91
+ [project.entry-points."db_graphql_gateway.adapters"]
92
+ duckdb = "my_duckdb_package.adapter:DuckDBAdapter"
93
+ ```
94
+ Users will then configure the gateway with `engine="duckdb"` and the plugin system will handle the rest.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Author
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ .PHONY: install lint typecheck test
2
+
3
+ install:
4
+ uv venv
5
+ uv pip install -e ".[dev,postgres]"
6
+
7
+ lint:
8
+ uv run ruff check src/ tests/
9
+ uv run ruff format --check src/ tests/
10
+
11
+ typecheck:
12
+ uv run mypy src/ tests/
13
+
14
+ test:
15
+ uv run pytest
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.5
2
+ Name: db-graphql-gateway
3
+ Version: 0.1.0
4
+ Summary: Automatically generate a secure, production-ready GraphQL API from a database connection.
5
+ Author-email: Mukesh M Lohar <mukesh1lohar@gmail.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: click>=8.1.0
14
+ Requires-Dist: pydantic>=2.0.0
15
+ Requires-Dist: pyjwt[crypto]>=2.8.0
16
+ Requires-Dist: pyyaml>=6.0.0
17
+ Requires-Dist: strawberry-graphql>=0.200.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: mypy; extra == 'dev'
20
+ Requires-Dist: pre-commit; extra == 'dev'
21
+ Requires-Dist: pytest; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio; extra == 'dev'
23
+ Requires-Dist: ruff; extra == 'dev'
24
+ Requires-Dist: testcontainers[postgres]>=4.4.1; extra == 'dev'
25
+ Provides-Extra: fastapi
26
+ Requires-Dist: fastapi>=0.141.1; extra == 'fastapi'
27
+ Requires-Dist: httpx>=0.28.1; extra == 'fastapi'
28
+ Provides-Extra: mysql
29
+ Requires-Dist: asyncmy>=0.2.14; extra == 'mysql'
30
+ Provides-Extra: postgres
31
+ Requires-Dist: asyncpg>=0.28.0; extra == 'postgres'
32
+ Provides-Extra: sqlite
33
+ Requires-Dist: aiosqlite>=0.22.1; extra == 'sqlite'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # db-graphql-gateway
37
+
38
+ Automatically generate a secure, production-ready GraphQL API from a database connection.
39
+
40
+ ## Features
41
+ - **Schema Discovery:** Automatically inspects your database schema and generates corresponding GraphQL types.
42
+ - **Relationships:** Resolves foreign keys to automatically wire up GraphQL associations.
43
+ - **Pagination:** Built-in Relay-style cursor pagination.
44
+ - **Authorization & Security:** Configurable JWT/OIDC support and query complexity/depth protection.
45
+ - **Performance:** Built-in DataLoader support to protect against N+1 query problems.
46
+
47
+ ## Getting Started
48
+ See documentation for full setup instructions.
@@ -0,0 +1,13 @@
1
+ # db-graphql-gateway
2
+
3
+ Automatically generate a secure, production-ready GraphQL API from a database connection.
4
+
5
+ ## Features
6
+ - **Schema Discovery:** Automatically inspects your database schema and generates corresponding GraphQL types.
7
+ - **Relationships:** Resolves foreign keys to automatically wire up GraphQL associations.
8
+ - **Pagination:** Built-in Relay-style cursor pagination.
9
+ - **Authorization & Security:** Configurable JWT/OIDC support and query complexity/depth protection.
10
+ - **Performance:** Built-in DataLoader support to protect against N+1 query problems.
11
+
12
+ ## Getting Started
13
+ See documentation for full setup instructions.
@@ -0,0 +1,27 @@
1
+ # STATE: db-graphql-gateway
2
+
3
+ ## Current Status
4
+ - Phase 1 (Foundation) has been successfully completed.
5
+ - Phase 2 (PostgreSQL Introspection) has been successfully completed.
6
+ - Phase 3 (Schema IR) has been successfully completed.
7
+ - Phase 4 (Basic GraphQL Generation) has been successfully completed.
8
+ - Phase 5 (Filter, Sort, Pagination) has been successfully completed.
9
+ - Phase 6 (Relationships & DataLoader) has been successfully completed.
10
+ - Phase 7 (Query Planner) has been successfully completed.
11
+ - Phase 8 (Authentication) has been successfully completed.
12
+ - Phase 9 (Authorization) has been successfully completed.
13
+ - Phase 10 (Security Hardening) has been successfully completed.
14
+ - Phase 11 (Mutations) has been successfully completed.
15
+ - Phase 12 (FastAPI/ORM Integrations) has been successfully completed.
16
+ - Phase 13 (CLI Tooling) has been successfully completed.
17
+ - [x] Phase 14 (Final Testing & Documentation) has been successfully completed.
18
+ - Developed end-to-end acceptance tests.
19
+ - Written extensive MkDocs documentation covering `ARCHITECTURE.md`, `SECURITY.md`, `BENCHMARKS.md`, and more.
20
+ - Successfully addressed strict typing with `pre-commit --all-files`.
21
+
22
+ ## Next Actions
23
+ - Project is effectively complete! All functionality, tests, and documentation are verified.
24
+ - Built a Docker-based integration test workflow with strict security and type checks, which runs 9 advanced test levels natively asserting against real postgres.
25
+
26
+ ## Issues / Blockers
27
+ - None.
@@ -0,0 +1,229 @@
1
+ # Architecture: db-graphql-gateway
2
+
3
+ The `db-graphql-gateway` is designed with a strict separation of concerns, decoupling the database
4
+ inspection and query compilation from the GraphQL presentation layer and authentication logic.
5
+
6
+ ---
7
+
8
+ ## 1. Core Execution Pipeline
9
+
10
+ The life cycle of a GraphQL query from the HTTP request down to the database engine follows this
11
+ strict pipeline:
12
+
13
+ ```mermaid
14
+ flowchart TB
15
+ classDef client fill:#e0f7fa,stroke:#006064,stroke-width:2px,color:#006064
16
+ classDef security fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#e65100
17
+ classDef core fill:#e8eaf6,stroke:#1a237e,stroke-width:2px,color:#1a237e
18
+ classDef db fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#1b5e20
19
+
20
+ Client([GraphQL Client]):::client -->|HTTP POST| Auth(Authentication Provider):::security
21
+
22
+ subgraph "GraphQL Gateway Pipeline"
23
+ Auth -->|Verified JWT| Context(Auth Context)
24
+ Context --> Resolver(Strawberry Resolver):::core
25
+
26
+ subgraph "Execution & Planning"
27
+ Resolver --> Limits{AST Limits}:::security
28
+ Limits --> Planner(Query Planner):::core
29
+ Planner --> DataLoader(DataLoader Registry):::core
30
+ end
31
+
32
+ subgraph "Security & Database Abstraction"
33
+ DataLoader --> AuthZ(Authorization Engine):::security
34
+ AuthZ --> |Inject SQL Policies| AdapterProtocol[[DatabaseAdapter Protocol]]:::core
35
+ end
36
+ end
37
+
38
+ AdapterProtocol --> PG[(PostgreSQL)]:::db
39
+ AdapterProtocol --> SQ[(SQLite)]:::db
40
+ AdapterProtocol --> MY[(MySQL/MariaDB)]:::db
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 2. Package Boundaries
46
+
47
+ The codebase is strictly separated into four functional areas:
48
+
49
+ <div class="grid cards" markdown>
50
+
51
+ - :material-database: **Database Abstraction**
52
+ ---
53
+ Located in `database/`. Handles adapters (`PostgresAdapter`, `SQLiteAdapter`,
54
+ `MySQLAdapter`), schema inspection, normalised models, and dialect-parameterised
55
+ query compilers. **No dialect-specific logic leaks above this layer.**
56
+
57
+ - :material-transit-connection-variant: **Schema Intermediate Representation**
58
+ ---
59
+ Located in `schema/ir/`. The source of truth mapping database structure to
60
+ GraphQL schemas. It is database-agnostic and GraphQL-agnostic.
61
+
62
+ - :material-graphql: **GraphQL Generation**
63
+ ---
64
+ Located in `graphql/`. Converts the IR into a Strawberry GraphQL schema (types,
65
+ queries, mutations) and handles execution, filtering, sorting, pagination.
66
+ Talks only to the IR and the `DatabaseAdapter` protocol — never to a dialect.
67
+
68
+ - :material-security: **Security & Auth**
69
+ ---
70
+ Located in `auth/` and `security/`. Validates callers (e.g., JWT), evaluates
71
+ contextual policies to generate SQL predicate trees (`FilterCondition` /
72
+ `FilterGroup`), and enforces query complexity budgets and AST limits. Produces
73
+ dialect-neutral predicate objects; placeholder style is owned by the compiler.
74
+
75
+ </div>
76
+
77
+ ---
78
+
79
+ ## 3. Multi-Dialect Adapter Architecture
80
+
81
+ ### 3.1 `DatabaseAdapter` Protocol
82
+
83
+ Every adapter implements the same protocol from `database/adapters/interfaces.py`:
84
+
85
+ ```python
86
+ class DatabaseAdapter(Protocol):
87
+ # ── Dialect capability flags ──────────────────────────────────────────
88
+ supports_returning: bool # RETURNING clause supported after DML
89
+ supports_upsert_on_conflict: bool # ON CONFLICT DO UPDATE / ON DUPLICATE KEY
90
+ placeholder_style: PlaceholderStyle # "numbered" | "qmark" | "named"
91
+ identifier_quote_char: str # '"' | '`' | '['
92
+
93
+ async def connect(self) -> None: ...
94
+ async def close(self) -> None: ...
95
+ async def execute(self, query: CompiledQuery) -> QueryResult: ...
96
+ async def execute_many(self, queries: list[CompiledQuery]) -> list[QueryResult]: ...
97
+ def inspector(self) -> SchemaInspector: ...
98
+ def compiler(self) -> QueryCompiler: ...
99
+ def type_mapper(self) -> TypeMapper: ...
100
+ ```
101
+
102
+ ### 3.2 `BaseQueryCompiler`
103
+
104
+ All SELECT / DML SQL generation lives in `database/adapters/base_compiler.py`.
105
+ Dialect subclasses only set three class-level attributes:
106
+
107
+ | Subclass | `placeholder_style` | `identifier_quote_char` | `supports_returning` |
108
+ |---|---|---|---|
109
+ | `PostgresQueryCompiler` | `"numbered"` (`$1, $2, …`) | `"` | `True` |
110
+ | `SQLiteQueryCompiler` | `"qmark"` (`?, ?, …`) | `"` | runtime-detected (≥ 3.35.0) |
111
+ | `MySQLQueryCompiler` | `"qmark"` (`?, ?, …`) | `` ` `` | `False` |
112
+
113
+ When `supports_returning = False`, `compile_mutation()` sets
114
+ `CompiledQuery.fetch_after_write = True` and the adapter's `execute()` issues a
115
+ follow-up `SELECT` using `cursor.lastrowid` (INSERT) or the known PK value
116
+ (UPDATE / DELETE). This pattern is called **SELECT-after-write**.
117
+
118
+ ### 3.3 Introspection
119
+
120
+ Each adapter ships its own `SchemaInspector` that reads dialect-specific metadata:
121
+
122
+ | Adapter | Introspection source |
123
+ |---|---|
124
+ | `PostgresSchemaInspector` | `pg_class`, `pg_attribute`, `pg_constraint`, `pg_enum` |
125
+ | `SQLiteSchemaInspector` | `PRAGMA table_info`, `PRAGMA foreign_key_list`, `sqlite_master` |
126
+ | `MySQLSchemaInspector` | `information_schema.columns`, `information_schema.key_column_usage` |
127
+
128
+ All three produce the **same** `DatabaseSchema → Table → Column` data model.
129
+ `IRBuilder.build()` is called identically regardless of which inspector ran.
130
+
131
+ ---
132
+
133
+ ## 4. Schema Intermediate Representation (IR)
134
+
135
+ The Gateway decouples the database schema from the GraphQL schema using the IR
136
+ (`GraphQLTypeIR`, `GraphQLFieldIR`).
137
+
138
+ 1. **Introspection (`inspector.py`)**: Reads dialect-specific system catalogs to
139
+ construct a `DatabaseSchema` with normalised `Column.type` strings.
140
+ 2. **Type mapping (`TypeMapper`)**: Each adapter's `TypeMapper` converts
141
+ normalised column types to abstract GraphQL scalar names (`"Int"`, `"Float"`,
142
+ `"String"`, `"DateTime"`, `"Boolean"`, `"JSON"`). No raw DB type strings leak
143
+ past the `TypeMapper`.
144
+ 3. **IR Build (`IRBuilder`)**: Converts the low-level schema into GraphQL-centric
145
+ constructs. During this phase, `GatewayConfig` overrides are applied.
146
+ **Sensitive fields** (e.g., `password`, `token`) are automatically redacted here
147
+ based on name-pattern matching — dialect-agnostically.
148
+
149
+ ---
150
+
151
+ ## 5. GraphQL Schema Generation (Build-Time)
152
+
153
+ The `GraphQLSchemaBuilder` transforms the IR into a Strawberry GraphQL schema
154
+ dynamically using Python's `type` function. This happens exactly **once at startup**.
155
+
156
+ ### Dynamic Annotations & Mypy
157
+ Because Strawberry relies heavily on Python's type hints (`__annotations__`) to
158
+ build the static GraphQL schema, the Builder must dynamically construct these
159
+ dictionaries for every resolver it generates.
160
+
161
+ ```python title="Injecting annotations dynamically to satisfy Strawberry"
162
+ update_fn.__annotations__ = {
163
+ "info": Info,
164
+ "id": pk_type,
165
+ "input": update_input_type,
166
+ "expected_version": Optional[int],
167
+ "return": Optional[sb_type],
168
+ }
169
+ ```
170
+
171
+ !!! note "Strict Typing"
172
+ This architecture cleanly separates the dynamic runtime from static analysis,
173
+ allowing the core engine to pass strict `mypy` checks.
174
+
175
+ ---
176
+
177
+ ## 6. Execution & DataLoading (Per-Request)
178
+
179
+ When a resolver executes, it does not use an ORM. It builds a `QueryPlan` that is
180
+ compiled directly into SQL via the adapter's `QueryCompiler`.
181
+
182
+ ### N+1 Query Elimination
183
+ Nested relationship fields use Strawberry DataLoaders that batch foreign keys at
184
+ execution time. The query compiler merges these batched IDs (e.g.,
185
+ `WHERE author_id IN ($1, $2)` or `WHERE author_id IN (?, ?)`) ensuring constant
186
+ $O(1)$ database execution regardless of the depth of the graph.
187
+
188
+ The `DataLoaderRegistry` receives a `schema_map` dict (`type_name → schema_name`)
189
+ from `GraphQLSchemaBuilder`, so relationship batch queries use the correct schema
190
+ name for each adapter (e.g., `"public"` for Postgres, `"main"` for SQLite).
191
+
192
+ ### Optimistic Concurrency & Soft Deletes
193
+ The `IRBuilder` automatically detects `version` and `deleted_at` columns.
194
+
195
+ - **Soft Deletes**: List queries automatically append `deleted_at IS NULL` filters,
196
+ and `delete_` mutations are converted into `update_` operations that set the
197
+ deletion timestamp.
198
+ - **Optimistic Locking**: Mutations include an `expected_version` argument. If
199
+ provided, the update query asserts `version = <expected>` and increments it,
200
+ failing if another transaction modified the row concurrently.
201
+
202
+ Both features are detected from **IR field names** (`f.name == "deleted_at"`,
203
+ `f.name == "version"`) — never from raw schema or adapter code.
204
+
205
+ ---
206
+
207
+ ## 7. Authorization Predicates
208
+
209
+ !!! danger "Zero Memory Filtering"
210
+ The Gateway **never** filters sensitive data in Python memory. All authorization
211
+ policies are pushed down to the database engine as SQL `WHERE` predicates,
212
+ regardless of which adapter is in use.
213
+
214
+ The `AuthorizationEngine` evaluates contextual policies and generates
215
+ `FilterCondition` / `FilterGroup` predicate trees. When the `QueryCompiler` builds
216
+ the final SQL string, it merges the basic user `WHERE` filters with the
217
+ authorization predicates.
218
+
219
+ The placeholder style (e.g., `$1` for Postgres, `?` for SQLite/MySQL) is owned
220
+ **exclusively by the compiler** — the auth engine produces dialect-neutral predicate
221
+ objects and has no knowledge of the underlying database engine.
222
+
223
+ For example, a policy defining `owner_id = $user_id` on the `tasks` table will
224
+ statically inject:
225
+
226
+ - `AND "owner_id" = $1` in Postgres SQL
227
+ - `AND "owner_id" = ?` in SQLite / MySQL SQL
228
+
229
+ In all cases, unauthorized rows are **never loaded into memory**.