fix(core): preserve data integrity and bound shared UI and response work
Module Package Release / publish-packages (push) Successful in 13s
Module Package Release / publish-packages (push) Successful in 13s
Release v0.1.46. Coordinated integrity review: GovOPlaN/govoplan-core#298.
This commit is contained in:
Executable
+147
@@ -0,0 +1,147 @@
|
||||
"""Exact, bound-parameter JSON permission predicates for SQLite/PostgreSQL.
|
||||
|
||||
These primitives only match strings (never coerced numbers/booleans) and
|
||||
objects inside actual arrays. Owners still define tenant, subject, permission,
|
||||
purpose and current-state policy. Unsupported dialects fail at compilation.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
|
||||
from sqlalchemy import Boolean, literal
|
||||
from sqlalchemy.exc import CompileError
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql.functions import FunctionElement
|
||||
|
||||
|
||||
class _ArrayString(FunctionElement):
|
||||
type = Boolean()
|
||||
inherit_cache = True
|
||||
|
||||
|
||||
class _ObjectStrings(FunctionElement):
|
||||
type = Boolean()
|
||||
inherit_cache = True
|
||||
|
||||
|
||||
class _ArrayObjectStrings(FunctionElement):
|
||||
type = Boolean()
|
||||
inherit_cache = True
|
||||
|
||||
|
||||
def json_array_contains_string(column, value: str):
|
||||
if type(value) is not str:
|
||||
raise TypeError("JSON string membership requires a string value.")
|
||||
return _ArrayString(column, literal(value))
|
||||
|
||||
|
||||
def _field_arguments(fields: Mapping[str, str]):
|
||||
if not isinstance(fields, Mapping) or not 1 <= len(fields) <= 16:
|
||||
raise ValueError("JSON object matching requires between 1 and 16 string fields.")
|
||||
arguments = []
|
||||
for key, value in sorted(fields.items()):
|
||||
if type(key) is not str or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", key) is None:
|
||||
raise ValueError("JSON object field names must be simple identifiers.")
|
||||
if type(value) is not str:
|
||||
raise TypeError("JSON object matching requires string values.")
|
||||
arguments.extend((literal(key), literal(value)))
|
||||
return arguments
|
||||
|
||||
|
||||
def json_object_matches_strings(column, fields: Mapping[str, str]):
|
||||
return _ObjectStrings(column, *_field_arguments(fields))
|
||||
|
||||
|
||||
def json_array_contains_object_strings(column, fields: Mapping[str, str]):
|
||||
return _ArrayObjectStrings(column, *_field_arguments(fields))
|
||||
|
||||
|
||||
@compiles(_ArrayString)
|
||||
@compiles(_ObjectStrings)
|
||||
@compiles(_ArrayObjectStrings)
|
||||
def _unsupported(element, compiler, **kwargs):
|
||||
raise CompileError("Exact JSON permission predicates support only SQLite and PostgreSQL.")
|
||||
|
||||
|
||||
def _parts(element, compiler, kwargs):
|
||||
return [compiler.process(item, **kwargs) for item in element.clauses]
|
||||
|
||||
|
||||
def _sqlite_array(value):
|
||||
return f"CASE WHEN json_type({value}) = 'array' THEN {value} ELSE '[]' END"
|
||||
|
||||
|
||||
def _postgres_array(value):
|
||||
value = f"CAST({value} AS JSON)"
|
||||
return f"CASE WHEN json_typeof({value}) = 'array' THEN {value} ELSE '[]'::json END"
|
||||
|
||||
|
||||
def _sqlite_fields(value, fields):
|
||||
terms = []
|
||||
for index in range(0, len(fields), 2):
|
||||
key, expected = fields[index:index + 2]
|
||||
path = f"('$.' || {key})"
|
||||
terms.extend((f"json_type({value}, {path}) = 'text'", f"json_extract({value}, {path}) = {expected}"))
|
||||
return " AND ".join(terms)
|
||||
|
||||
|
||||
def _postgres_fields(value, fields):
|
||||
terms = []
|
||||
for index in range(0, len(fields), 2):
|
||||
key, expected = fields[index:index + 2]
|
||||
terms.extend((f"json_typeof(({value}) -> {key}) = 'string'", f"(({value}) ->> {key}) = {expected}"))
|
||||
return " AND ".join(terms)
|
||||
|
||||
|
||||
@compiles(_ArrayString, "sqlite")
|
||||
def _array_string_sqlite(element, compiler, **kwargs):
|
||||
value, expected = _parts(element, compiler, kwargs)
|
||||
return (
|
||||
f"EXISTS (SELECT 1 FROM json_each({_sqlite_array(value)}) AS gp_json_string "
|
||||
f"WHERE gp_json_string.type = 'text' AND gp_json_string.value = {expected})"
|
||||
)
|
||||
|
||||
|
||||
@compiles(_ArrayString, "postgresql")
|
||||
def _array_string_postgres(element, compiler, **kwargs):
|
||||
value, expected = _parts(element, compiler, kwargs)
|
||||
return (
|
||||
f"EXISTS (SELECT 1 FROM json_array_elements({_postgres_array(value)}) AS gp_json_string(value) "
|
||||
f"WHERE json_typeof(gp_json_string.value) = 'string' "
|
||||
f"AND (gp_json_string.value #>> '{{}}') = {expected})"
|
||||
)
|
||||
|
||||
|
||||
@compiles(_ObjectStrings, "sqlite")
|
||||
def _object_sqlite(element, compiler, **kwargs):
|
||||
value, *fields = _parts(element, compiler, kwargs)
|
||||
value = f"CASE WHEN json_type({value}) = 'object' THEN {value} ELSE '{{}}' END"
|
||||
return f"({_sqlite_fields(value, fields)})"
|
||||
|
||||
|
||||
@compiles(_ObjectStrings, "postgresql")
|
||||
def _object_postgres(element, compiler, **kwargs):
|
||||
value, *fields = _parts(element, compiler, kwargs)
|
||||
value = f"CAST({value} AS JSON)"
|
||||
return f"(json_typeof({value}) = 'object' AND {_postgres_fields(value, fields)})"
|
||||
|
||||
|
||||
@compiles(_ArrayObjectStrings, "sqlite")
|
||||
def _array_object_sqlite(element, compiler, **kwargs):
|
||||
value, *fields = _parts(element, compiler, kwargs)
|
||||
item = "CASE WHEN gp_json_object.type = 'object' THEN gp_json_object.value ELSE '{}' END"
|
||||
return (
|
||||
f"EXISTS (SELECT 1 FROM json_each({_sqlite_array(value)}) AS gp_json_object "
|
||||
f"WHERE {_sqlite_fields(item, fields)})"
|
||||
)
|
||||
|
||||
|
||||
@compiles(_ArrayObjectStrings, "postgresql")
|
||||
def _array_object_postgres(element, compiler, **kwargs):
|
||||
value, *fields = _parts(element, compiler, kwargs)
|
||||
return (
|
||||
f"EXISTS (SELECT 1 FROM json_array_elements({_postgres_array(value)}) AS gp_json_object(value) "
|
||||
f"WHERE json_typeof(gp_json_object.value) = 'object' "
|
||||
f"AND {_postgres_fields('gp_json_object.value', fields)})"
|
||||
)
|
||||
Reference in New Issue
Block a user