metaobjects 0.9.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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""FastAPI filter-allowlist codegen — one ``<entity_snake>_filter_allowlist.py``
|
|
2
|
+
per writable entity (``source.rdb`` with ``@kind="table"``).
|
|
3
|
+
|
|
4
|
+
FR-009 §3.5 (cross-port). Emits a per-entity static allowlist:
|
|
5
|
+
|
|
6
|
+
* ``<ENTITY>_FILTER_FIELDS: frozenset[str]`` — the field names that are
|
|
7
|
+
filterable (gate against ``invalid_filter_field``).
|
|
8
|
+
* ``<ENTITY>_FILTER_OPS_BY_FIELD: dict[str, frozenset[str]]`` — the
|
|
9
|
+
per-field operator vocabulary (gate against ``invalid_filter_op``).
|
|
10
|
+
|
|
11
|
+
Authoring contract: only fields with ``@filterable: true`` appear in the
|
|
12
|
+
allowlist. If no field is marked filterable, the file is still emitted (with
|
|
13
|
+
empty frozensets) so the generated router can unconditionally delegate to
|
|
14
|
+
it without conditional codegen branching.
|
|
15
|
+
|
|
16
|
+
Operators-per-subtype mapping (FR-009 §5, identical across ports):
|
|
17
|
+
|
|
18
|
+
* ``string`` / ``enum`` → ``eq, ne, in, like, isNull``
|
|
19
|
+
* ``uuid`` → ``eq, ne, in, isNull`` (no ``like`` — not a substring type, no ordering)
|
|
20
|
+
* ``int / long / float / double / decimal / currency / date / timestamp / time``
|
|
21
|
+
→ ``eq, ne, gt, gte, lt, lte, in, isNull``
|
|
22
|
+
* ``boolean`` → ``eq, isNull``
|
|
23
|
+
|
|
24
|
+
``object`` fields are skipped — they have no SQL column surface that filters
|
|
25
|
+
can target.
|
|
26
|
+
|
|
27
|
+
Mirrors the Java ``SpringFilterAllowlistGenerator`` + Kotlin
|
|
28
|
+
``KotlinFilterAllowlistGenerator`` + C# allowlist emission. View kinds
|
|
29
|
+
(``view`` / ``materializedView`` / ``storedProc`` / ``tableFunction``) get
|
|
30
|
+
no allowlist — they share the "no router emitted" gate with
|
|
31
|
+
``router_generator``.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from metaobjects.codegen.constants import generated_header
|
|
36
|
+
from metaobjects.codegen.format import ruff_format
|
|
37
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator, per_entity
|
|
38
|
+
from metaobjects.codegen.generators.m2m_codegen import build_object_index
|
|
39
|
+
from metaobjects.codegen.generators.tph_plan import tph_plan_for
|
|
40
|
+
from metaobjects.codegen.instance_artifacts import emits_instance_artifacts
|
|
41
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
42
|
+
from metaobjects.meta.core.field.meta_field import MetaField
|
|
43
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
44
|
+
from metaobjects.meta.persistence.source.meta_source import MetaSource
|
|
45
|
+
from metaobjects.meta.persistence.source.source_constants import SOURCE_KIND_TABLE
|
|
46
|
+
from metaobjects.shared.base_types import TYPE_SOURCE
|
|
47
|
+
from metaobjects.shared.separators import PACKAGE_SEP
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Operator sets — preserve insertion order (Python dict order == spec order).
|
|
51
|
+
_OPS_STRING: tuple[str, ...] = ("eq", "ne", "in", "like", "isNull")
|
|
52
|
+
# uuid: identity-comparison only — no `like` (not a substring type), no ordering.
|
|
53
|
+
_OPS_UUID: tuple[str, ...] = ("eq", "ne", "in", "isNull")
|
|
54
|
+
_OPS_NUMERIC: tuple[str, ...] = ("eq", "ne", "gt", "gte", "lt", "lte", "in", "isNull")
|
|
55
|
+
_OPS_BOOLEAN: tuple[str, ...] = ("eq", "isNull")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _effective_fqn(entity: MetaObject) -> str:
|
|
59
|
+
"""``package::name``, resolving package from the nearest ancestor that carries
|
|
60
|
+
one. Mirror of the router generator's helper of the same name."""
|
|
61
|
+
pkg = entity.package
|
|
62
|
+
parent = entity.parent
|
|
63
|
+
while pkg is None and parent is not None:
|
|
64
|
+
pkg = parent.package
|
|
65
|
+
parent = parent.parent
|
|
66
|
+
return f"{pkg}{PACKAGE_SEP}{entity.name}" if pkg else entity.name
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _snake_case(name: str) -> str:
|
|
70
|
+
"""``Author`` → ``author``; ``AuthorBrief`` → ``author_brief``. Mirror of
|
|
71
|
+
the router generator's helper of the same name."""
|
|
72
|
+
out: list[str] = []
|
|
73
|
+
for i, ch in enumerate(name):
|
|
74
|
+
if ch.isupper() and i > 0:
|
|
75
|
+
out.append("_")
|
|
76
|
+
out.append(ch.lower())
|
|
77
|
+
return "".join(out)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _primary_source_rdb(entity: MetaObject) -> MetaSource | None:
|
|
81
|
+
"""Return the entity's ``source.rdb`` child (own only), or ``None``."""
|
|
82
|
+
for c in entity.own_children():
|
|
83
|
+
if c.type == TYPE_SOURCE and isinstance(c, MetaSource):
|
|
84
|
+
return c
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ops_for_subtype_ordered(sub_type: str | None) -> tuple[str, ...]:
|
|
89
|
+
"""Return the operator tuple for ``sub_type`` per the FR-009 §5 matrix.
|
|
90
|
+
|
|
91
|
+
Ordered (canonical operator order), unlike the loader's
|
|
92
|
+
``validation_passes.ops_for_subtype`` which returns an order-free
|
|
93
|
+
``frozenset``. This is the single ordered source the codegen
|
|
94
|
+
filter-allowlist emit + the cross-port ``field.filter-ops`` conformance
|
|
95
|
+
capability consume.
|
|
96
|
+
|
|
97
|
+
Returns an empty tuple for subtypes outside the FR-009 vocabulary
|
|
98
|
+
(defensive — the allowlist effectively becomes a "field is unknown" gate
|
|
99
|
+
when the subtype is unrecognized).
|
|
100
|
+
|
|
101
|
+
Module-level back-compat shim; the override seam is
|
|
102
|
+
:meth:`FilterAllowlistGenerator._ops_for_field`.
|
|
103
|
+
"""
|
|
104
|
+
if sub_type is None:
|
|
105
|
+
return ()
|
|
106
|
+
if sub_type in (fc.FIELD_SUBTYPE_STRING, fc.FIELD_SUBTYPE_ENUM):
|
|
107
|
+
return _OPS_STRING
|
|
108
|
+
if sub_type == fc.FIELD_SUBTYPE_UUID:
|
|
109
|
+
return _OPS_UUID
|
|
110
|
+
if sub_type in (
|
|
111
|
+
fc.FIELD_SUBTYPE_INT,
|
|
112
|
+
fc.FIELD_SUBTYPE_LONG,
|
|
113
|
+
fc.FIELD_SUBTYPE_FLOAT,
|
|
114
|
+
fc.FIELD_SUBTYPE_DOUBLE,
|
|
115
|
+
fc.FIELD_SUBTYPE_DECIMAL,
|
|
116
|
+
fc.FIELD_SUBTYPE_CURRENCY,
|
|
117
|
+
fc.FIELD_SUBTYPE_DATE,
|
|
118
|
+
fc.FIELD_SUBTYPE_TIMESTAMP,
|
|
119
|
+
fc.FIELD_SUBTYPE_TIME,
|
|
120
|
+
):
|
|
121
|
+
return _OPS_NUMERIC
|
|
122
|
+
if sub_type == fc.FIELD_SUBTYPE_BOOLEAN:
|
|
123
|
+
return _OPS_BOOLEAN
|
|
124
|
+
return ()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_filterable(field: MetaField) -> bool:
|
|
128
|
+
"""True iff ``field`` carries ``@filterable: true`` as a metadata attribute.
|
|
129
|
+
|
|
130
|
+
Accepts a real boolean ``True`` or the string ``"true"`` (case-insensitive),
|
|
131
|
+
matching the Java/Kotlin/C# tolerance for either YAML-bool or raw-string
|
|
132
|
+
value forms.
|
|
133
|
+
"""
|
|
134
|
+
raw = field.attr(fc.FIELD_ATTR_FILTERABLE)
|
|
135
|
+
if raw is True:
|
|
136
|
+
return True
|
|
137
|
+
if isinstance(raw, str):
|
|
138
|
+
return raw.lower() == "true"
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _compute_filterable_ops(entity: MetaObject) -> dict[str, tuple[str, ...]]:
|
|
143
|
+
"""Module-level back-compat wrapper. Delegates to a default
|
|
144
|
+
:class:`FilterAllowlistGenerator`."""
|
|
145
|
+
return FilterAllowlistGenerator()._compute_filterable_ops(entity)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class FilterAllowlistGenerator:
|
|
149
|
+
"""``object.entity`` + ``source.rdb @kind="table"`` → one
|
|
150
|
+
``<entity_snake>_filter_allowlist.py`` per writable entity (FR-009 §3.5).
|
|
151
|
+
|
|
152
|
+
EXTENSION SEAM (open-for-extension). Adopters subclass this and override one of
|
|
153
|
+
the protected hooks to customize the emitted allowlist without forking. The
|
|
154
|
+
factory ``filter_allowlist_generator()`` and the module-level
|
|
155
|
+
``render_filter_allowlist()`` both delegate to a default instance, so the
|
|
156
|
+
default suite stays byte-identical.
|
|
157
|
+
|
|
158
|
+
Override points:
|
|
159
|
+
|
|
160
|
+
* ``_ops_for_field(field)`` — the operator tuple a filterable field is allowed
|
|
161
|
+
(defaults to the FR-009 §5 per-subtype matrix). Override to widen/narrow the
|
|
162
|
+
operator vocabulary for a custom field shape.
|
|
163
|
+
* ``_emit_constants(fields_const, ops_const, ops_by_field)`` — the emitted
|
|
164
|
+
``frozenset`` / ``dict`` literal lines.
|
|
165
|
+
* ``render_filter_allowlist(entity)`` — the whole module (last resort).
|
|
166
|
+
|
|
167
|
+
Skips entities without a ``source.rdb`` child and read-only kinds
|
|
168
|
+
(view / materializedView / storedProc / tableFunction) — same gate as
|
|
169
|
+
``RouterGenerator``, so the two generators emit in lock-step.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
name = "filter-allowlist-generator"
|
|
173
|
+
|
|
174
|
+
def _ops_for_field(self, field: MetaField) -> tuple[str, ...]:
|
|
175
|
+
"""The operator tuple allowed for ``field`` (FR-009 §5 per-subtype matrix by
|
|
176
|
+
default). Override to customize the per-field operator vocabulary; returning
|
|
177
|
+
an empty tuple excludes the field from the allowlist."""
|
|
178
|
+
return ops_for_subtype_ordered(field.sub_type)
|
|
179
|
+
|
|
180
|
+
def _compute_filterable_ops(
|
|
181
|
+
self, entity: MetaObject, object_index: dict[str, MetaObject] | None = None
|
|
182
|
+
) -> dict[str, tuple[str, ...]]:
|
|
183
|
+
"""Build the ``{field_name: op_tuple}`` map for ``entity`` (own + inherited
|
|
184
|
+
effective fields). Only fields with ``@filterable: true`` are included;
|
|
185
|
+
object fields and fields whose ``_ops_for_field`` yields no operators are
|
|
186
|
+
skipped. Preserves declaration order so the emitted source is deterministic.
|
|
187
|
+
|
|
188
|
+
FR-017 TPH: when ``entity`` is a discriminator base (``object_index`` given),
|
|
189
|
+
the union also folds in each subtype's own filterable fields, so the single
|
|
190
|
+
table's per-subtype routes (and the polymorphic base) can filter on a
|
|
191
|
+
subtype-only column. Non-TPH entities are unaffected."""
|
|
192
|
+
out: dict[str, tuple[str, ...]] = {}
|
|
193
|
+
|
|
194
|
+
def add(f: MetaField) -> None:
|
|
195
|
+
if f.sub_type == fc.FIELD_SUBTYPE_OBJECT or f.name in out or not _is_filterable(f):
|
|
196
|
+
return
|
|
197
|
+
ops = self._ops_for_field(f)
|
|
198
|
+
if ops:
|
|
199
|
+
out[f.name] = ops
|
|
200
|
+
|
|
201
|
+
for f in entity.fields():
|
|
202
|
+
add(f)
|
|
203
|
+
if object_index is not None:
|
|
204
|
+
plan = tph_plan_for(entity, object_index)
|
|
205
|
+
if plan is not None:
|
|
206
|
+
for st in plan.subtypes:
|
|
207
|
+
for f in st.entity.own_fields():
|
|
208
|
+
add(f)
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
def _emit_constants(
|
|
212
|
+
self,
|
|
213
|
+
fields_const: str,
|
|
214
|
+
ops_const: str,
|
|
215
|
+
ops_by_field: dict[str, tuple[str, ...]],
|
|
216
|
+
) -> list[str]:
|
|
217
|
+
"""The ``<ENTITY>_FILTER_FIELDS`` frozenset + ``<ENTITY>_FILTER_OPS_BY_FIELD``
|
|
218
|
+
dict literal lines. Override to change the emitted data-structure shape."""
|
|
219
|
+
lines: list[str] = []
|
|
220
|
+
if not ops_by_field:
|
|
221
|
+
lines.append(f"{fields_const}: frozenset[str] = frozenset()")
|
|
222
|
+
lines.append("")
|
|
223
|
+
lines.append(f"{ops_const}: dict[str, frozenset[str]] = {{}}")
|
|
224
|
+
else:
|
|
225
|
+
lines.append(f"{fields_const}: frozenset[str] = frozenset({{")
|
|
226
|
+
for name in ops_by_field:
|
|
227
|
+
lines.append(f' "{name}",')
|
|
228
|
+
lines.append("})")
|
|
229
|
+
lines.append("")
|
|
230
|
+
lines.append(f"{ops_const}: dict[str, frozenset[str]] = {{")
|
|
231
|
+
for name, ops in ops_by_field.items():
|
|
232
|
+
ops_literal = ", ".join(f'"{op}"' for op in ops)
|
|
233
|
+
lines.append(f' "{name}": frozenset({{{ops_literal}}}),')
|
|
234
|
+
lines.append("}")
|
|
235
|
+
return lines
|
|
236
|
+
|
|
237
|
+
def render_filter_allowlist(
|
|
238
|
+
self, entity: MetaObject, object_index: dict[str, MetaObject] | None = None
|
|
239
|
+
) -> str | None:
|
|
240
|
+
"""Render the filter allowlist module for ``entity`` (or ``None`` to skip).
|
|
241
|
+
|
|
242
|
+
Returns ``None`` for entities without a ``source.rdb`` child and for
|
|
243
|
+
read-only kinds (``view`` / ``materializedView`` / ``storedProc`` /
|
|
244
|
+
``tableFunction``) — these match the router generator's "no router"
|
|
245
|
+
gate, so emitting an allowlist would be pure noise.
|
|
246
|
+
|
|
247
|
+
``object_index`` (optional) lets a TPH discriminator base fold in its
|
|
248
|
+
subtypes' filterable fields (see :meth:`_compute_filterable_ops`).
|
|
249
|
+
"""
|
|
250
|
+
if not emits_instance_artifacts(entity):
|
|
251
|
+
return None
|
|
252
|
+
src = _primary_source_rdb(entity)
|
|
253
|
+
if src is None:
|
|
254
|
+
return None
|
|
255
|
+
if src.effective_kind() != SOURCE_KIND_TABLE:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
short_name = entity.name
|
|
259
|
+
upper = short_name.upper()
|
|
260
|
+
fields_const = f"{upper}_FILTER_FIELDS"
|
|
261
|
+
ops_const = f"{upper}_FILTER_OPS_BY_FIELD"
|
|
262
|
+
|
|
263
|
+
ops_by_field = self._compute_filterable_ops(entity, object_index)
|
|
264
|
+
|
|
265
|
+
parts: list[str] = []
|
|
266
|
+
parts.append(
|
|
267
|
+
generated_header(short_name, _effective_fqn(entity)).rstrip() + "\n"
|
|
268
|
+
+ f'"""GENERATED — per-entity FR-009 filter allowlist for {short_name}.\n\n'
|
|
269
|
+
f"{fields_const} lists the filterable field names; {ops_const}\n"
|
|
270
|
+
f'constrains the operator vocabulary for each field by its subtype."""\n'
|
|
271
|
+
)
|
|
272
|
+
parts.append("from __future__ import annotations")
|
|
273
|
+
parts.append("")
|
|
274
|
+
parts.extend(self._emit_constants(fields_const, ops_const, ops_by_field))
|
|
275
|
+
parts.append("")
|
|
276
|
+
return "\n".join(parts)
|
|
277
|
+
|
|
278
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
279
|
+
index = build_object_index(ctx.entities)
|
|
280
|
+
|
|
281
|
+
def emit(entity: MetaObject, _c: GenContext) -> list[EmittedFile]:
|
|
282
|
+
source = self.render_filter_allowlist(entity, index)
|
|
283
|
+
if source is None:
|
|
284
|
+
return []
|
|
285
|
+
snake = _snake_case(entity.name)
|
|
286
|
+
return [
|
|
287
|
+
EmittedFile(
|
|
288
|
+
path=f"{snake}_filter_allowlist.py",
|
|
289
|
+
content=ruff_format(source),
|
|
290
|
+
)
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
return per_entity(emit)(ctx)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def render_filter_allowlist(
|
|
297
|
+
entity: MetaObject, object_index: dict[str, MetaObject] | None = None
|
|
298
|
+
) -> str | None:
|
|
299
|
+
"""Module-level back-compat wrapper. Delegates to a default
|
|
300
|
+
:class:`FilterAllowlistGenerator`. Subclass it to customize."""
|
|
301
|
+
return FilterAllowlistGenerator().render_filter_allowlist(entity, object_index)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def filter_allowlist_generator() -> Generator:
|
|
305
|
+
"""Generator factory: ``object.entity`` + ``source.rdb @kind="table"`` → one
|
|
306
|
+
``<entity_snake>_filter_allowlist.py`` per writable entity.
|
|
307
|
+
|
|
308
|
+
Returns a :class:`FilterAllowlistGenerator` (subclassable extension seam)."""
|
|
309
|
+
return FilterAllowlistGenerator()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Shared M:N (many-to-many) codegen descriptor — the build-time resolution of a
|
|
2
|
+
source entity's ``relationship.* @cardinality:"many" + @through`` children into
|
|
3
|
+
the concrete coordinates each Python generator needs (Pydantic nested collection,
|
|
4
|
+
FastAPI traversal route, and the physical junction/target column names the
|
|
5
|
+
repository seam joins on).
|
|
6
|
+
|
|
7
|
+
This mirrors the TS reference ``relation-resolver.ts`` (Unit 10): codegen derives
|
|
8
|
+
the descriptor at build time from the junction entity's two ``identity.reference``
|
|
9
|
+
children (the cross-port SSOT for FK direction) via the shared
|
|
10
|
+
``derive_m2m_fields`` helper — the relationship never restates its FK columns.
|
|
11
|
+
|
|
12
|
+
The descriptor carries:
|
|
13
|
+
* ``relation_name`` — the navigation member name (route segment + Pydantic field).
|
|
14
|
+
* ``target_entity`` — the related entity's bare name (Pydantic element type).
|
|
15
|
+
* ``source_plural`` / ``target_plural`` — pluralized entity names for the
|
|
16
|
+
REST contract: the source URL segment is the ENTITY name pluralized
|
|
17
|
+
(``Person`` → ``persons``), NOT the physical ``@table`` (cross-port grammar).
|
|
18
|
+
* ``source_column`` / ``target_column`` — the PHYSICAL junction FK columns
|
|
19
|
+
(e.g. ``follower_id`` / ``followee_id``); the repository seam joins on these.
|
|
20
|
+
* ``junction_table`` / ``target_table`` / ``target_pk_column`` — physical names
|
|
21
|
+
for the join.
|
|
22
|
+
* ``symmetric`` — selects the union-on-read traversal.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
29
|
+
from metaobjects.meta.core.field.meta_field import MetaField
|
|
30
|
+
from metaobjects.meta.meta_data import MetaData
|
|
31
|
+
from metaobjects.meta.core.identity.identity_constants import (
|
|
32
|
+
IDENTITY_ATTR_FIELDS,
|
|
33
|
+
IDENTITY_SUBTYPE_PRIMARY,
|
|
34
|
+
)
|
|
35
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
36
|
+
from metaobjects.meta.core.relationship.derive_m2m_fields import (
|
|
37
|
+
M2MDerivationError,
|
|
38
|
+
derive_m2m_fields,
|
|
39
|
+
)
|
|
40
|
+
from metaobjects.meta.core.relationship.meta_relationship import MetaRelationship
|
|
41
|
+
from metaobjects.meta.core.relationship.relationship_constants import CARDINALITY_MANY
|
|
42
|
+
from metaobjects.meta.persistence.source.meta_source import MetaSource
|
|
43
|
+
from metaobjects.shared.base_types import TYPE_IDENTITY, TYPE_SOURCE
|
|
44
|
+
from metaobjects.shared.separators import PACKAGE_SEP
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class M2mDescriptor:
|
|
49
|
+
"""Build-time coordinates of one M:N navigation on a source entity."""
|
|
50
|
+
|
|
51
|
+
relation_name: str
|
|
52
|
+
target_entity: str
|
|
53
|
+
source_plural: str
|
|
54
|
+
target_plural: str
|
|
55
|
+
junction_table: str
|
|
56
|
+
target_table: str
|
|
57
|
+
source_column: str
|
|
58
|
+
target_column: str
|
|
59
|
+
target_pk_column: str
|
|
60
|
+
symmetric: bool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _strip_package(name: str) -> str:
|
|
64
|
+
idx = name.rfind(PACKAGE_SEP)
|
|
65
|
+
return name[idx + len(PACKAGE_SEP):] if idx >= 0 else name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def plural_lowercase(name: str) -> str:
|
|
69
|
+
"""``Person`` → ``persons``. Cross-port-aligned trivial pluralization — the
|
|
70
|
+
SOURCE URL segment is the entity name pluralized, NOT the physical table."""
|
|
71
|
+
return name.lower() + "s"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _primary_source_rdb(entity: MetaObject) -> MetaSource | None:
|
|
75
|
+
for c in entity.own_children():
|
|
76
|
+
if c.type == TYPE_SOURCE and isinstance(c, MetaSource):
|
|
77
|
+
return c
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _physical_table(entity: MetaObject) -> str:
|
|
82
|
+
"""The entity's physical SQL table name (``source.rdb`` physical name),
|
|
83
|
+
falling back to the entity name when no source is declared."""
|
|
84
|
+
src = _primary_source_rdb(entity)
|
|
85
|
+
if src is not None:
|
|
86
|
+
name = src.physical_name()
|
|
87
|
+
if name:
|
|
88
|
+
return name
|
|
89
|
+
return entity.name
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _column_of(field: MetaField | None, fallback: str) -> str:
|
|
93
|
+
"""Physical column name for a metadata field name: its ``@column`` override,
|
|
94
|
+
else the field's own name. Mirrors the runtime ``object_manager._column_of``
|
|
95
|
+
(no implicit snake_case — the metadata field name IS the column unless
|
|
96
|
+
``@column`` overrides)."""
|
|
97
|
+
if field is None:
|
|
98
|
+
return fallback
|
|
99
|
+
col = field.attr(fc.FIELD_ATTR_COLUMN)
|
|
100
|
+
return col if isinstance(col, str) and col else field.name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _field_named(entity: MetaObject, field_name: str) -> MetaField | None:
|
|
104
|
+
for f in entity.fields():
|
|
105
|
+
if f.name == field_name:
|
|
106
|
+
return f
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _pk_field_name(entity: MetaObject) -> str:
|
|
111
|
+
"""The single PK field name (``identity.primary @fields``), default ``id``."""
|
|
112
|
+
for c in entity.children():
|
|
113
|
+
if c.type == TYPE_IDENTITY and c.sub_type == IDENTITY_SUBTYPE_PRIMARY:
|
|
114
|
+
fields = c.attr(IDENTITY_ATTR_FIELDS)
|
|
115
|
+
if isinstance(fields, (list, tuple)) and fields:
|
|
116
|
+
first = fields[0]
|
|
117
|
+
if isinstance(first, str):
|
|
118
|
+
return first
|
|
119
|
+
if isinstance(fields, str) and fields:
|
|
120
|
+
return fields.split(",")[0].strip() or "id"
|
|
121
|
+
return "id"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def m2m_relationships(entity: MetaObject) -> list[MetaRelationship]:
|
|
125
|
+
"""The entity's own ``relationship.* @cardinality:"many" + @through`` children
|
|
126
|
+
(declaration order). 1:N relationships (no ``@through``) are excluded."""
|
|
127
|
+
out: list[MetaRelationship] = []
|
|
128
|
+
for c in entity.own_children():
|
|
129
|
+
if not isinstance(c, MetaRelationship):
|
|
130
|
+
continue
|
|
131
|
+
if c.cardinality() != CARDINALITY_MANY:
|
|
132
|
+
continue
|
|
133
|
+
if c.through() is None:
|
|
134
|
+
continue
|
|
135
|
+
out.append(c)
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def resolve_m2m_descriptors(
|
|
140
|
+
entity: MetaObject, object_index: dict[str, MetaObject]
|
|
141
|
+
) -> list[M2mDescriptor]:
|
|
142
|
+
"""Resolve every M:N navigation on *entity* into a build-time descriptor.
|
|
143
|
+
|
|
144
|
+
*object_index* is a bare-name → entity map of the loaded model. A relationship
|
|
145
|
+
that fails derivation (missing/malformed junction, ambiguous self-join) raises
|
|
146
|
+
:class:`M2MDerivationError` — the loader validates this; reaching it here is a
|
|
147
|
+
metadata error, surfaced loudly (never silently skipped).
|
|
148
|
+
"""
|
|
149
|
+
descriptors: list[M2mDescriptor] = []
|
|
150
|
+
for rel in m2m_relationships(entity):
|
|
151
|
+
target_name = _strip_package(rel.object_ref() or "")
|
|
152
|
+
junction_name = _strip_package(rel.through() or "")
|
|
153
|
+
target = object_index.get(target_name)
|
|
154
|
+
junction = object_index.get(junction_name)
|
|
155
|
+
if target is None or junction is None:
|
|
156
|
+
raise M2MDerivationError(
|
|
157
|
+
f'M:N relationship "{entity.name}.{rel.name}": @objectRef '
|
|
158
|
+
f'"{target_name}" / @through "{junction_name}" must resolve to '
|
|
159
|
+
f"loaded entities"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Widen to dict[str, MetaData] for the shared derivation helper (dict is
|
|
163
|
+
# invariant; MetaObject is a MetaData subtype).
|
|
164
|
+
meta_index: dict[str, MetaData] = dict(object_index)
|
|
165
|
+
fields = derive_m2m_fields(rel, entity, meta_index)
|
|
166
|
+
source_fk = _field_named(junction, fields.source_field)
|
|
167
|
+
target_fk = _field_named(junction, fields.target_field)
|
|
168
|
+
|
|
169
|
+
descriptors.append(
|
|
170
|
+
M2mDescriptor(
|
|
171
|
+
relation_name=rel.name,
|
|
172
|
+
target_entity=target.name,
|
|
173
|
+
source_plural=plural_lowercase(entity.name),
|
|
174
|
+
target_plural=plural_lowercase(target.name),
|
|
175
|
+
junction_table=_physical_table(junction),
|
|
176
|
+
target_table=_physical_table(target),
|
|
177
|
+
source_column=_column_of(source_fk, fields.source_field),
|
|
178
|
+
target_column=_column_of(target_fk, fields.target_field),
|
|
179
|
+
target_pk_column=_column_of(
|
|
180
|
+
_field_named(target, _pk_field_name(target)),
|
|
181
|
+
_pk_field_name(target),
|
|
182
|
+
),
|
|
183
|
+
symmetric=rel.symmetric(),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
return descriptors
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_object_index(entities: list[MetaObject]) -> dict[str, MetaObject]:
|
|
190
|
+
"""Bare-name → entity map (the codegen resolution surface, mirroring the
|
|
191
|
+
runtime resolver's ``object_index``)."""
|
|
192
|
+
return {e.name: e for e in entities}
|