jsonql-py 0.1.0__py3-none-any.whl
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.
- jsonql/__init__.py +142 -0
- jsonql/adapters/__init__.py +42 -0
- jsonql/adapters/base.py +300 -0
- jsonql/adapters/django_adapter.py +98 -0
- jsonql/adapters/django_mongo.py +94 -0
- jsonql/adapters/fastapi_adapter.py +102 -0
- jsonql/adapters/fastapi_mongo.py +66 -0
- jsonql/adapters/flask_adapter.py +84 -0
- jsonql/adapters/flask_mongo.py +67 -0
- jsonql/adapters/mongo_base.py +389 -0
- jsonql/builder.py +137 -0
- jsonql/conditions.py +103 -0
- jsonql/dialect.py +162 -0
- jsonql/driver.py +26 -0
- jsonql/engine.py +185 -0
- jsonql/errors.py +63 -0
- jsonql/factory.py +357 -0
- jsonql/hydrator.py +170 -0
- jsonql/logger.py +57 -0
- jsonql/mongo_driver.py +82 -0
- jsonql/mongo_transpiler.py +337 -0
- jsonql/parser.py +185 -0
- jsonql/transpiler.py +634 -0
- jsonql/types.py +185 -0
- jsonql/validator.py +226 -0
- jsonql_py-0.1.0.dist-info/METADATA +261 -0
- jsonql_py-0.1.0.dist-info/RECORD +29 -0
- jsonql_py-0.1.0.dist-info/WHEEL +4 -0
- jsonql_py-0.1.0.dist-info/licenses/LICENSE +21 -0
jsonql/__init__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""JSONQL Python SDK — A JSON-based query language for SQL databases."""
|
|
2
|
+
|
|
3
|
+
from .builder import MutationBuilder, QueryBuilder
|
|
4
|
+
from .conditions import (
|
|
5
|
+
and_,
|
|
6
|
+
contains,
|
|
7
|
+
ends_with,
|
|
8
|
+
eq,
|
|
9
|
+
field,
|
|
10
|
+
gt,
|
|
11
|
+
gte,
|
|
12
|
+
is_in,
|
|
13
|
+
like,
|
|
14
|
+
lt,
|
|
15
|
+
lte,
|
|
16
|
+
neq,
|
|
17
|
+
not_,
|
|
18
|
+
not_in,
|
|
19
|
+
or_,
|
|
20
|
+
starts_with,
|
|
21
|
+
)
|
|
22
|
+
from .dialect import (
|
|
23
|
+
MySQLDialect,
|
|
24
|
+
PostgresDialect,
|
|
25
|
+
SQLDialect,
|
|
26
|
+
SQLiteDialect,
|
|
27
|
+
new_dialect,
|
|
28
|
+
)
|
|
29
|
+
from .driver import DatabaseDriver
|
|
30
|
+
from .engine import JsonQLEngine
|
|
31
|
+
from .errors import (
|
|
32
|
+
AdapterError,
|
|
33
|
+
JsonQLError,
|
|
34
|
+
JsonQLExecutionError,
|
|
35
|
+
JsonQLTranspileError,
|
|
36
|
+
JsonQLValidationError,
|
|
37
|
+
)
|
|
38
|
+
from .factory import (
|
|
39
|
+
connect_mongo,
|
|
40
|
+
create_driver,
|
|
41
|
+
create_driver_with_dsn,
|
|
42
|
+
env_or,
|
|
43
|
+
load_schema,
|
|
44
|
+
must_connect_mongo,
|
|
45
|
+
must_load_schema,
|
|
46
|
+
)
|
|
47
|
+
from .hydrator import ResultHydrator
|
|
48
|
+
from .logger import ConsoleLogger, Logger, NoOpLogger
|
|
49
|
+
from .mongo_driver import MongoDBDriver
|
|
50
|
+
from .mongo_transpiler import MongoResult, MongoTranspiler
|
|
51
|
+
from .parser import Parser, ParserOptions
|
|
52
|
+
from .transpiler import SQLTranspiler
|
|
53
|
+
from .types import (
|
|
54
|
+
DistinctOption,
|
|
55
|
+
JsonQLField,
|
|
56
|
+
JsonQLMutation,
|
|
57
|
+
JsonQLQuery,
|
|
58
|
+
JsonQLRelation,
|
|
59
|
+
JsonQLSchema,
|
|
60
|
+
JsonQLSettings,
|
|
61
|
+
JsonQLTable,
|
|
62
|
+
TranspileResult,
|
|
63
|
+
ValidationError,
|
|
64
|
+
ValidationResult,
|
|
65
|
+
parse_schema,
|
|
66
|
+
)
|
|
67
|
+
from .validator import Validator
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
# Types
|
|
71
|
+
"JsonQLQuery",
|
|
72
|
+
"JsonQLMutation",
|
|
73
|
+
"DistinctOption",
|
|
74
|
+
"JsonQLSchema",
|
|
75
|
+
"JsonQLSettings",
|
|
76
|
+
"JsonQLTable",
|
|
77
|
+
"JsonQLField",
|
|
78
|
+
"JsonQLRelation",
|
|
79
|
+
"TranspileResult",
|
|
80
|
+
"ValidationError",
|
|
81
|
+
"ValidationResult",
|
|
82
|
+
"parse_schema",
|
|
83
|
+
# Core
|
|
84
|
+
"Parser",
|
|
85
|
+
"ParserOptions",
|
|
86
|
+
"SQLTranspiler",
|
|
87
|
+
"Validator",
|
|
88
|
+
"ResultHydrator",
|
|
89
|
+
# Dialect
|
|
90
|
+
"SQLDialect",
|
|
91
|
+
"PostgresDialect",
|
|
92
|
+
"MySQLDialect",
|
|
93
|
+
"SQLiteDialect",
|
|
94
|
+
"new_dialect",
|
|
95
|
+
# Builder
|
|
96
|
+
"QueryBuilder",
|
|
97
|
+
"MutationBuilder",
|
|
98
|
+
# Conditions
|
|
99
|
+
"eq",
|
|
100
|
+
"neq",
|
|
101
|
+
"gt",
|
|
102
|
+
"gte",
|
|
103
|
+
"lt",
|
|
104
|
+
"lte",
|
|
105
|
+
"is_in",
|
|
106
|
+
"not_in",
|
|
107
|
+
"like",
|
|
108
|
+
"contains",
|
|
109
|
+
"starts_with",
|
|
110
|
+
"ends_with",
|
|
111
|
+
"field",
|
|
112
|
+
"and_",
|
|
113
|
+
"or_",
|
|
114
|
+
"not_",
|
|
115
|
+
# Engine / Driver
|
|
116
|
+
"DatabaseDriver",
|
|
117
|
+
"JsonQLEngine",
|
|
118
|
+
# MongoDB
|
|
119
|
+
"MongoTranspiler",
|
|
120
|
+
"MongoResult",
|
|
121
|
+
"MongoDBDriver",
|
|
122
|
+
# Factory / Helpers
|
|
123
|
+
"env_or",
|
|
124
|
+
"load_schema",
|
|
125
|
+
"must_load_schema",
|
|
126
|
+
"create_driver",
|
|
127
|
+
"create_driver_with_dsn",
|
|
128
|
+
"connect_mongo",
|
|
129
|
+
"must_connect_mongo",
|
|
130
|
+
# Errors
|
|
131
|
+
"JsonQLError",
|
|
132
|
+
"JsonQLValidationError",
|
|
133
|
+
"JsonQLTranspileError",
|
|
134
|
+
"JsonQLExecutionError",
|
|
135
|
+
"AdapterError",
|
|
136
|
+
# Logger
|
|
137
|
+
"Logger",
|
|
138
|
+
"ConsoleLogger",
|
|
139
|
+
"NoOpLogger",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""JSONQL framework adapters — Flask, FastAPI, Django REST + MongoDB variants."""
|
|
2
|
+
|
|
3
|
+
from ..errors import AdapterError
|
|
4
|
+
from .base import (
|
|
5
|
+
AdapterOptions,
|
|
6
|
+
BaseHandler,
|
|
7
|
+
)
|
|
8
|
+
from .base import (
|
|
9
|
+
_infer_mutation as infer_mutation,
|
|
10
|
+
)
|
|
11
|
+
from .django_adapter import JsonQLDjangoView
|
|
12
|
+
from .django_mongo import JsonQLDjangoMongoView
|
|
13
|
+
from .fastapi_adapter import create_fastapi_router
|
|
14
|
+
from .fastapi_mongo import create_fastapi_mongo_router
|
|
15
|
+
from .flask_adapter import create_flask_blueprint
|
|
16
|
+
from .flask_mongo import create_flask_mongo_blueprint
|
|
17
|
+
from .mongo_base import (
|
|
18
|
+
MongoAdapterOptions,
|
|
19
|
+
MongoBaseHandler,
|
|
20
|
+
build_rest_mutation,
|
|
21
|
+
get_id_from_query,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# SQL adapters
|
|
26
|
+
"BaseHandler",
|
|
27
|
+
"AdapterOptions",
|
|
28
|
+
"create_flask_blueprint",
|
|
29
|
+
"create_fastapi_router",
|
|
30
|
+
"JsonQLDjangoView",
|
|
31
|
+
# MongoDB adapters
|
|
32
|
+
"MongoBaseHandler",
|
|
33
|
+
"MongoAdapterOptions",
|
|
34
|
+
"create_flask_mongo_blueprint",
|
|
35
|
+
"create_fastapi_mongo_router",
|
|
36
|
+
"JsonQLDjangoMongoView",
|
|
37
|
+
# Shared helpers
|
|
38
|
+
"AdapterError",
|
|
39
|
+
"infer_mutation",
|
|
40
|
+
"get_id_from_query",
|
|
41
|
+
"build_rest_mutation",
|
|
42
|
+
]
|
jsonql/adapters/base.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Framework-agnostic base handler for the JSONQL request pipeline.
|
|
2
|
+
|
|
3
|
+
Subclasses only need to provide framework-specific input extraction
|
|
4
|
+
and error creation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Awaitable, Callable
|
|
11
|
+
|
|
12
|
+
from ..driver import DatabaseDriver
|
|
13
|
+
from ..hydrator import ResultHydrator
|
|
14
|
+
from ..logger import ConsoleLogger, Logger, NoOpLogger
|
|
15
|
+
from ..parser import Parser
|
|
16
|
+
from ..transpiler import SQLTranspiler
|
|
17
|
+
from ..types import JsonQLMutation, JsonQLQuery, JsonQLSchema, is_mutation
|
|
18
|
+
from ..validator import Validator
|
|
19
|
+
|
|
20
|
+
# Type aliases for lifecycle hooks
|
|
21
|
+
Hook = Callable[..., Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AdapterOptions:
|
|
26
|
+
"""Configuration for a JSONQL adapter."""
|
|
27
|
+
|
|
28
|
+
schema: JsonQLSchema | None = None
|
|
29
|
+
schema_resolver: Callable[..., JsonQLSchema | None] | None = None
|
|
30
|
+
driver: DatabaseDriver | None = None
|
|
31
|
+
execute: Callable[[str, list[Any]], Awaitable[list[dict[str, Any]]]] | None = None
|
|
32
|
+
dialect: str = "sqlite"
|
|
33
|
+
|
|
34
|
+
debug: bool = False
|
|
35
|
+
logger: Logger | None = None
|
|
36
|
+
|
|
37
|
+
tables: list[str] | dict[str, str] | None = None
|
|
38
|
+
|
|
39
|
+
# Mutation status resolver — determines HTTP status for mutations.
|
|
40
|
+
# Signature: (op: str, context: Any) -> int
|
|
41
|
+
# Default behaviour (when None): 201 for "create", 200 for update/delete.
|
|
42
|
+
mutation_status: Callable[[str, Any], int] | None = None
|
|
43
|
+
|
|
44
|
+
# Lifecycle hooks
|
|
45
|
+
before_parse: Hook | None = None
|
|
46
|
+
after_parse: Hook | None = None
|
|
47
|
+
before_query: Hook | None = None
|
|
48
|
+
before_validate: Hook | None = None
|
|
49
|
+
after_validate: Hook | None = None
|
|
50
|
+
before_hydrate: Hook | None = None
|
|
51
|
+
after_hydrate: Hook | None = None
|
|
52
|
+
after_query: Hook | None = None
|
|
53
|
+
|
|
54
|
+
# Mutation hooks
|
|
55
|
+
before_create: Hook | None = None
|
|
56
|
+
after_create: Hook | None = None
|
|
57
|
+
before_update: Hook | None = None
|
|
58
|
+
after_update: Hook | None = None
|
|
59
|
+
before_delete: Hook | None = None
|
|
60
|
+
after_delete: Hook | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Re-export public helpers from mongo_base (shared between SQL and MongoDB)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _infer_mutation(http_method: str, raw: dict[str, Any]) -> dict[str, Any]:
|
|
67
|
+
"""Inject ``op`` into the raw query based on the HTTP method."""
|
|
68
|
+
if "op" in raw:
|
|
69
|
+
return raw
|
|
70
|
+
|
|
71
|
+
if "create" in raw:
|
|
72
|
+
raw["op"] = "create"
|
|
73
|
+
if "data" not in raw:
|
|
74
|
+
raw["data"] = raw.get("create")
|
|
75
|
+
return raw
|
|
76
|
+
if "update" in raw:
|
|
77
|
+
raw["op"] = "update"
|
|
78
|
+
if "patch" not in raw:
|
|
79
|
+
raw["patch"] = raw.get("update")
|
|
80
|
+
return raw
|
|
81
|
+
if "delete" in raw:
|
|
82
|
+
raw["op"] = "delete"
|
|
83
|
+
return raw
|
|
84
|
+
|
|
85
|
+
upsert = raw.get("upsert")
|
|
86
|
+
if isinstance(upsert, dict):
|
|
87
|
+
if "where" in upsert and "update" in upsert:
|
|
88
|
+
raw["op"] = "update"
|
|
89
|
+
raw["where"] = upsert["where"]
|
|
90
|
+
raw["patch"] = upsert["update"]
|
|
91
|
+
return raw
|
|
92
|
+
if "create" in upsert:
|
|
93
|
+
raw["op"] = "create"
|
|
94
|
+
raw["data"] = upsert["create"]
|
|
95
|
+
return raw
|
|
96
|
+
|
|
97
|
+
method = http_method.upper()
|
|
98
|
+
if method == "POST" and "data" in raw:
|
|
99
|
+
raw["op"] = "create"
|
|
100
|
+
elif method in ("PUT", "PATCH") and ("patch" in raw or "where" in raw):
|
|
101
|
+
raw["op"] = "update"
|
|
102
|
+
elif method == "DELETE" and "where" in raw:
|
|
103
|
+
raw["op"] = "delete"
|
|
104
|
+
return raw
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BaseHandler:
|
|
108
|
+
"""Core pipeline: parse → validate → transpile → execute → hydrate."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, options: AdapterOptions) -> None:
|
|
111
|
+
self.options = options
|
|
112
|
+
self.parser = Parser()
|
|
113
|
+
self.can_execute = bool(options.execute or options.driver)
|
|
114
|
+
|
|
115
|
+
dialect = options.dialect
|
|
116
|
+
if options.driver:
|
|
117
|
+
dialect = options.driver.dialect
|
|
118
|
+
|
|
119
|
+
self.transpiler = SQLTranspiler(dialect) if self.can_execute else None
|
|
120
|
+
self.hydrator = ResultHydrator() if self.can_execute else None
|
|
121
|
+
|
|
122
|
+
if options.logger:
|
|
123
|
+
self.logger: Logger = options.logger
|
|
124
|
+
elif options.debug:
|
|
125
|
+
self.logger = ConsoleLogger()
|
|
126
|
+
else:
|
|
127
|
+
self.logger = NoOpLogger()
|
|
128
|
+
|
|
129
|
+
async def process_request(
|
|
130
|
+
self,
|
|
131
|
+
raw_input: Any,
|
|
132
|
+
context: Any,
|
|
133
|
+
http_method: str,
|
|
134
|
+
path_name: str,
|
|
135
|
+
) -> Any:
|
|
136
|
+
"""Run the full JSONQL pipeline and return the result dict."""
|
|
137
|
+
raw_query = raw_input
|
|
138
|
+
|
|
139
|
+
# 1. beforeParse
|
|
140
|
+
if self.options.before_parse:
|
|
141
|
+
raw_query = await _await_maybe(self.options.before_parse(raw_query, context))
|
|
142
|
+
|
|
143
|
+
# 2. Infer mutation
|
|
144
|
+
if isinstance(raw_query, dict):
|
|
145
|
+
raw_query = _infer_mutation(http_method, raw_query)
|
|
146
|
+
|
|
147
|
+
# 3. Parse
|
|
148
|
+
statement = self.parser.parse(raw_query)
|
|
149
|
+
|
|
150
|
+
if self.options.after_parse:
|
|
151
|
+
statement = await _await_maybe(self.options.after_parse(statement, context))
|
|
152
|
+
|
|
153
|
+
# 4. Resolve table name
|
|
154
|
+
table_name: str | None = None
|
|
155
|
+
if isinstance(statement, JsonQLQuery):
|
|
156
|
+
table_name = statement.from_table or None
|
|
157
|
+
elif isinstance(statement, JsonQLMutation):
|
|
158
|
+
table_name = None # mutations don't carry from_table
|
|
159
|
+
|
|
160
|
+
table_name = self._resolve_table(statement, table_name, path_name)
|
|
161
|
+
|
|
162
|
+
if isinstance(statement, JsonQLQuery) and table_name and not statement.from_table:
|
|
163
|
+
statement.from_table = table_name
|
|
164
|
+
|
|
165
|
+
# 5. beforeQuery
|
|
166
|
+
if self.options.before_query:
|
|
167
|
+
statement = await _await_maybe(self.options.before_query(statement, context))
|
|
168
|
+
|
|
169
|
+
# 6. beforeValidate
|
|
170
|
+
if self.options.before_validate:
|
|
171
|
+
statement = await _await_maybe(self.options.before_validate(statement, context))
|
|
172
|
+
|
|
173
|
+
# 7. Resolve schema
|
|
174
|
+
schema: JsonQLSchema | None = None
|
|
175
|
+
if self.options.schema_resolver:
|
|
176
|
+
schema = await _await_maybe(self.options.schema_resolver(context))
|
|
177
|
+
else:
|
|
178
|
+
schema = self.options.schema
|
|
179
|
+
|
|
180
|
+
# 8. Validate
|
|
181
|
+
if schema and table_name and isinstance(statement, JsonQLQuery):
|
|
182
|
+
table_def = schema.tables.get(table_name)
|
|
183
|
+
if table_def and table_def.fields:
|
|
184
|
+
validator = Validator(schema, table_name)
|
|
185
|
+
validation = validator.validate(statement)
|
|
186
|
+
|
|
187
|
+
if self.options.after_validate:
|
|
188
|
+
await _await_maybe(self.options.after_validate(validation, context))
|
|
189
|
+
|
|
190
|
+
if not validation.valid:
|
|
191
|
+
return {
|
|
192
|
+
"error": "Validation Error",
|
|
193
|
+
"details": [
|
|
194
|
+
{"code": e.code, "message": e.message, "path": e.path}
|
|
195
|
+
for e in validation.errors
|
|
196
|
+
],
|
|
197
|
+
}, 400
|
|
198
|
+
|
|
199
|
+
# 9. Execute
|
|
200
|
+
if self.can_execute and self.transpiler and table_name:
|
|
201
|
+
is_mut = is_mutation(statement)
|
|
202
|
+
|
|
203
|
+
# Mutation before-hooks
|
|
204
|
+
if is_mut and isinstance(statement, JsonQLMutation):
|
|
205
|
+
if statement.op == "create" and self.options.before_create:
|
|
206
|
+
statement = await _await_maybe(self.options.before_create(statement, context))
|
|
207
|
+
elif statement.op == "update" and self.options.before_update:
|
|
208
|
+
statement = await _await_maybe(self.options.before_update(statement, context))
|
|
209
|
+
elif statement.op == "delete" and self.options.before_delete:
|
|
210
|
+
statement = await _await_maybe(self.options.before_delete(statement, context))
|
|
211
|
+
|
|
212
|
+
# Transpile
|
|
213
|
+
result = self.transpiler.transpile(statement, table_name, schema)
|
|
214
|
+
self.logger.debug(f"[JSONQL] SQL: {result.sql}")
|
|
215
|
+
self.logger.debug(f"[JSONQL] Params: {result.args}")
|
|
216
|
+
|
|
217
|
+
# Execute
|
|
218
|
+
flat_rows: list[dict[str, Any]] = []
|
|
219
|
+
try:
|
|
220
|
+
if self.options.driver:
|
|
221
|
+
flat_rows = await self.options.driver.query(result.sql, result.args)
|
|
222
|
+
elif self.options.execute:
|
|
223
|
+
flat_rows = await self.options.execute(result.sql, result.args)
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
self.logger.error(f"[JSONQL] Execution error: {exc}")
|
|
226
|
+
return {"error": "Execution Error", "details": str(exc)}, 400
|
|
227
|
+
|
|
228
|
+
# Mutation after-hooks
|
|
229
|
+
if is_mut and isinstance(statement, JsonQLMutation):
|
|
230
|
+
data: Any = {"meta": {"query": raw_input}, "data": flat_rows}
|
|
231
|
+
if statement.op == "create" and self.options.after_create:
|
|
232
|
+
data = await _await_maybe(self.options.after_create(data, context))
|
|
233
|
+
elif statement.op == "update" and self.options.after_update:
|
|
234
|
+
data = await _await_maybe(self.options.after_update(data, context))
|
|
235
|
+
elif statement.op == "delete" and self.options.after_delete:
|
|
236
|
+
data = await _await_maybe(self.options.after_delete(data, context))
|
|
237
|
+
if self.options.after_query:
|
|
238
|
+
data = await _await_maybe(self.options.after_query(data, context))
|
|
239
|
+
|
|
240
|
+
# Determine HTTP status: custom resolver → default 200
|
|
241
|
+
if self.options.mutation_status:
|
|
242
|
+
status = await _await_maybe(
|
|
243
|
+
self.options.mutation_status(statement.op or "", context)
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
status = 200
|
|
247
|
+
return data, status
|
|
248
|
+
|
|
249
|
+
# Hydrate
|
|
250
|
+
if self.hydrator:
|
|
251
|
+
if self.options.before_hydrate:
|
|
252
|
+
flat_rows = await _await_maybe(self.options.before_hydrate(flat_rows, context))
|
|
253
|
+
|
|
254
|
+
hydrated = self.hydrator.hydrate(flat_rows, schema, table_name)
|
|
255
|
+
|
|
256
|
+
if self.options.after_hydrate:
|
|
257
|
+
hydrated = await _await_maybe(self.options.after_hydrate(hydrated, context))
|
|
258
|
+
|
|
259
|
+
result_data: Any = {"meta": {"query": raw_input}, "data": hydrated}
|
|
260
|
+
if self.options.after_query:
|
|
261
|
+
result_data = await _await_maybe(self.options.after_query(result_data, context))
|
|
262
|
+
return result_data, 200
|
|
263
|
+
|
|
264
|
+
return None, 200
|
|
265
|
+
|
|
266
|
+
def _resolve_table(
|
|
267
|
+
self,
|
|
268
|
+
statement: Any,
|
|
269
|
+
table_name: str | None,
|
|
270
|
+
path_name: str,
|
|
271
|
+
) -> str | None:
|
|
272
|
+
tables = self.options.tables
|
|
273
|
+
|
|
274
|
+
if tables is None:
|
|
275
|
+
return table_name or path_name or None
|
|
276
|
+
|
|
277
|
+
if isinstance(tables, list):
|
|
278
|
+
if not table_name:
|
|
279
|
+
table_name = path_name
|
|
280
|
+
if table_name not in tables:
|
|
281
|
+
raise ValueError(f"Table '{table_name}' is not allowed")
|
|
282
|
+
return table_name
|
|
283
|
+
|
|
284
|
+
# dict mapping
|
|
285
|
+
mapped = tables.get(path_name)
|
|
286
|
+
if mapped:
|
|
287
|
+
return mapped
|
|
288
|
+
if not path_name and table_name:
|
|
289
|
+
if table_name not in tables.values():
|
|
290
|
+
raise ValueError(f"Table '{table_name}' is not allowed")
|
|
291
|
+
return table_name
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def _await_maybe(value: Any) -> Any:
|
|
295
|
+
"""Await a value if it is a coroutine, otherwise return it directly."""
|
|
296
|
+
import asyncio
|
|
297
|
+
|
|
298
|
+
if asyncio.iscoroutine(value) or asyncio.isfuture(value):
|
|
299
|
+
return await value
|
|
300
|
+
return value
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Django adapter — a class-based view for JSONQL.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
# urls.py
|
|
6
|
+
from jsonql.adapters import JsonQLDjangoView, AdapterOptions
|
|
7
|
+
|
|
8
|
+
options = AdapterOptions(
|
|
9
|
+
dialect="postgres",
|
|
10
|
+
execute=run_sql,
|
|
11
|
+
schema=my_schema,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
urlpatterns = [
|
|
15
|
+
path("jsonql/", JsonQLDjangoView.as_view(options=options)),
|
|
16
|
+
path("jsonql/<path:path>/", JsonQLDjangoView.as_view(options=options)),
|
|
17
|
+
]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
from typing import Any, ClassVar
|
|
25
|
+
|
|
26
|
+
from ..errors import AdapterError, JsonQLError
|
|
27
|
+
from .base import AdapterOptions, BaseHandler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_raw_input(request: Any) -> Any:
|
|
31
|
+
"""Extract the JSONQL query from a Django request.
|
|
32
|
+
|
|
33
|
+
- GET with ``?q=<json>`` → parse the JSON string
|
|
34
|
+
- GET without ``q`` → use query params as dict
|
|
35
|
+
- POST/PUT/PATCH/DELETE → use the JSON body
|
|
36
|
+
"""
|
|
37
|
+
if request.method == "GET":
|
|
38
|
+
q = request.GET.get("q")
|
|
39
|
+
if q:
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(q)
|
|
42
|
+
except (ValueError, json.JSONDecodeError):
|
|
43
|
+
return q
|
|
44
|
+
if request.GET:
|
|
45
|
+
return dict(request.GET)
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(request.body) if request.body else {}
|
|
49
|
+
except (json.JSONDecodeError, ValueError):
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class JsonQLDjangoView:
|
|
54
|
+
"""Django class-based view for JSONQL.
|
|
55
|
+
|
|
56
|
+
Designed to work with or without Django REST Framework.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
options: ClassVar[AdapterOptions]
|
|
60
|
+
|
|
61
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
62
|
+
self._handler: BaseHandler | None = None
|
|
63
|
+
for key, value in kwargs.items():
|
|
64
|
+
setattr(self, key, value)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def as_view(cls, *, options: AdapterOptions | None = None, **initkwargs: Any) -> Any:
|
|
68
|
+
"""Return a Django view function."""
|
|
69
|
+
from django.http import JsonResponse
|
|
70
|
+
|
|
71
|
+
handler = BaseHandler(options or cls.options)
|
|
72
|
+
|
|
73
|
+
def _run(coro: Any) -> Any:
|
|
74
|
+
loop = asyncio.new_event_loop()
|
|
75
|
+
try:
|
|
76
|
+
return loop.run_until_complete(coro)
|
|
77
|
+
finally:
|
|
78
|
+
loop.close()
|
|
79
|
+
|
|
80
|
+
def view(request: Any, path: str = "", **kwargs: Any) -> Any:
|
|
81
|
+
try:
|
|
82
|
+
raw_input = _extract_raw_input(request)
|
|
83
|
+
result, status = _run(
|
|
84
|
+
handler.process_request(raw_input, request, request.method, path)
|
|
85
|
+
)
|
|
86
|
+
return JsonResponse(result, status=status, safe=False)
|
|
87
|
+
except AdapterError as exc:
|
|
88
|
+
return JsonResponse({"error": str(exc)}, status=exc.status)
|
|
89
|
+
except (ValueError, TypeError, JsonQLError) as exc:
|
|
90
|
+
return JsonResponse({"error": str(exc)}, status=400)
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
if handler.logger:
|
|
93
|
+
handler.logger.error(f"[JSONQL] Unhandled error: {exc}")
|
|
94
|
+
return JsonResponse({"error": str(exc)}, status=500)
|
|
95
|
+
|
|
96
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
97
|
+
|
|
98
|
+
return csrf_exempt(view)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Django adapter for MongoDB — a class-based view for JSONQL.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
# urls.py
|
|
6
|
+
from jsonql.adapters import JsonQLDjangoMongoView, MongoAdapterOptions
|
|
7
|
+
from jsonql import must_connect_mongo, must_load_schema
|
|
8
|
+
|
|
9
|
+
client, db = must_connect_mongo("mongodb://localhost:27017", "mydb")
|
|
10
|
+
schema = must_load_schema("schema.json")
|
|
11
|
+
|
|
12
|
+
options = MongoAdapterOptions(database=db, schema=schema)
|
|
13
|
+
|
|
14
|
+
urlpatterns = [
|
|
15
|
+
path("jsonql/", JsonQLDjangoMongoView.as_view(options=options)),
|
|
16
|
+
path("jsonql/<path:path>/", JsonQLDjangoMongoView.as_view(options=options)),
|
|
17
|
+
]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
from typing import Any, ClassVar
|
|
25
|
+
|
|
26
|
+
from ..errors import AdapterError, JsonQLError
|
|
27
|
+
from .mongo_base import MongoAdapterOptions, MongoBaseHandler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_raw_input(request: Any) -> Any:
|
|
31
|
+
"""Extract the JSONQL query from a Django request.
|
|
32
|
+
|
|
33
|
+
- GET with ``?q=<json>`` → parse the JSON string
|
|
34
|
+
- GET without ``q`` → use query params as dict
|
|
35
|
+
- POST/PUT/PATCH/DELETE → use the JSON body
|
|
36
|
+
"""
|
|
37
|
+
if request.method == "GET":
|
|
38
|
+
q = request.GET.get("q")
|
|
39
|
+
if q:
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(q)
|
|
42
|
+
except (ValueError, json.JSONDecodeError):
|
|
43
|
+
return q
|
|
44
|
+
if request.GET:
|
|
45
|
+
return dict(request.GET)
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(request.body) if request.body else {}
|
|
49
|
+
except (json.JSONDecodeError, ValueError):
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class JsonQLDjangoMongoView:
|
|
54
|
+
"""Django class-based view for JSONQL with MongoDB."""
|
|
55
|
+
|
|
56
|
+
options: ClassVar[MongoAdapterOptions]
|
|
57
|
+
|
|
58
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
59
|
+
for key, value in kwargs.items():
|
|
60
|
+
setattr(self, key, value)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def as_view(cls, *, options: MongoAdapterOptions | None = None, **initkwargs: Any) -> Any:
|
|
64
|
+
"""Return a Django view function."""
|
|
65
|
+
from django.http import JsonResponse
|
|
66
|
+
|
|
67
|
+
handler = MongoBaseHandler(options or cls.options)
|
|
68
|
+
|
|
69
|
+
def _run(coro: Any) -> Any:
|
|
70
|
+
loop = asyncio.new_event_loop()
|
|
71
|
+
try:
|
|
72
|
+
return loop.run_until_complete(coro)
|
|
73
|
+
finally:
|
|
74
|
+
loop.close()
|
|
75
|
+
|
|
76
|
+
def view(request: Any, path: str = "", **kwargs: Any) -> Any:
|
|
77
|
+
try:
|
|
78
|
+
raw_input = _extract_raw_input(request)
|
|
79
|
+
result, status = _run(
|
|
80
|
+
handler.process_request(raw_input, request, request.method, path)
|
|
81
|
+
)
|
|
82
|
+
return JsonResponse(result, status=status, safe=False)
|
|
83
|
+
except AdapterError as exc:
|
|
84
|
+
return JsonResponse({"error": str(exc)}, status=exc.status)
|
|
85
|
+
except (ValueError, TypeError, JsonQLError) as exc:
|
|
86
|
+
return JsonResponse({"error": str(exc)}, status=400)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
if handler.logger:
|
|
89
|
+
handler.logger.error(f"[JSONQL] Unhandled error: {exc}")
|
|
90
|
+
return JsonResponse({"error": str(exc)}, status=500)
|
|
91
|
+
|
|
92
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
93
|
+
|
|
94
|
+
return csrf_exempt(view)
|