v7.14.2 - #599
Merged
Merged
Conversation
…ource)
Replace the monolithic main_api.py backend with a modular generator that emits
a slim main_api.py (app + router includes) plus one routers/<class>.py per
resource, a shared database.py, and bal_stdlib.py. The output is far easier to
read, navigate and maintain than a single ~100 KB file, and it is now the single
source of truth for FastAPI backend generation.
- besser/generators/backend: new generate_modular_api + templates
(router/main_api/database/bal_stdlib). Full feature parity with the old
monolithic template: association-class endpoints (composite-key CRUD, links as
{target, <attrs>} with PUT reconciliation, add/get/remove relationship
endpoints), real primary keys (a declared is_id PK of any name, incl. one named
`id`, is client-supplied; a surrogate id stays server-owned), OCL validators
via the shared Pydantic generator, and method bodies normalized + compile-gated.
- RESTAPIGenerator(backend=True) now delegates to BackendGenerator; the
backend=False simple rest_api.py path is unchanged. The monolithic
backend_fast_api_template.py.j2 is removed.
- sql_alchemy: main_api.py and sql_alchemy.py now share one database via the
DATABASE_URL environment variable (default ./data/<model>.db), so the API and
the ORM never touch two different databases.
- tests: test_backend_assoc_class.py runs an 11-assertion association-class
contract against the per-file generator; test_backend_modular_layout.py covers
the file layout; test_backend.py / test_backend_full_uml.py updated for the new
structure. Full suite green.
…close review gaps
- HTTPException handler carries the endpoint's real message in `detail`
again (the generated frontend reads response.data.detail); regression
test added - the old tests only checked response.text, which the
still-correct `message` field satisfied
- PUT primary-key guard compares against the class's real PK instead of a
hardcoded 'id', so Room.number can no longer be rewritten through PUT
(orphaning the association-class rows that point at it); e2e test added
- /search/ covers inherited attributes, so a subclass without own
attributes keeps its route instead of falling through to /{id}/ as a 422
- RESTAPIGenerator(backend=True) renders only the API layer via the shared
generate_modular_api (no longer overwriting sql_alchemy.py /
pydantic_classes.py it never owned) and honors its port argument;
BackendGenerator gains an explicit port parameter that wins over the
docker config; delegation contract tests added
- pk-type mapping deduplicated into structural_utils.get_pk_py_types,
shared by the backend and SQLAlchemy generators
- database.py only creates the data/ folder for SQLite URLs
- api_generator docstring states the deliberate behavior changes instead
of claiming a purely structural split; docs updated for the modular
layout (backend.rst, rest_api.rst, backend_example.rst,
full_web_app.rst, terraform_example.rst)
…nerator feat(backend): modular per-file FastAPI generator (one router per resource)
Deleting a Booking that had ReservedRoom links crashed the generated
backend with SQLAlchemy's "Dependency rule tried to blank-out primary key
column" AssertionError: the link's FK is part of its composite primary
key, so the ORM could neither null it nor knew to delete the row. And the
generated table could not delete a link row at all, because it guessed the
row id from the first column (the price) and called /reservedroom/88.0/.
- sql_alchemy: the entity -> links relationship of every association class
carries cascade="all, delete-orphan" (both ends), so deleting an entity
deletes its link rows; the link -> entity side stays cascade-free.
- backend routers: DELETE returns a snapshot of the deleted row's columns
instead of the live ORM object, whose cascade-loaded back-references
made the JSON encoder recurse endlessly.
- react: the table binding names its row key fields (declared is_id
attribute, or both foreign keys in the backend's route order for an
association class) and getRowId builds the path from them, so
DELETE/PUT of a Room hits /room/{number}/ and of a link
/reservedroom/{booking_id}/{rooms_id}/.
Tests: end-to-end cascade from both ends and link deletion (backend),
row_key_fields serialization (react), cascade emission (sqlalchemy).
Verified live on the hotel-booking model.
fix(generators): deleting an entity deletes its association-class links
…d edit
The create endpoint refused a Booking without guests ("At least 1 Guest(s)
required"), but every later path could strip them: PUT with guests=[],
DELETE of the relationship, DELETE of the last Guest itself. The same held
for association-class links (a Booking's last Room) and for mandatory
to-one ends, where deleting the target crashed on NOT NULL instead of
being refused. The ORM cannot express a lower bound on the many side, so
the routers enforce it:
- DELETE /<entity>/{id}/ is refused with 409 when a related entity would
be left below the minimum it requires of this class (secondary tables,
association-class links, FK ends, inherited associations alike).
- DELETE of a relationship or of an association-class row checks both
sides of the link.
- PUT applies the create endpoint's "At least N required" check to
secondary-table and association-class ends, and refuses removing a
link the other side still needs.
- PUT for a one-to-many end (Employee.manages) now diffs the current and
requested members instead of nulling every FK first: with a mandatory
managed_by the null-out violated NOT NULL even when nothing changed, so
an Employee could not be edited at all. Detaching a member that requires
the parent is refused with 409; attaching/detaching the rest works.
End-to-end tests on a hotel-like model cover every path.
The N:M reconciliation dereferenced the optional list unconditionally, so a PUT without e.g. `staying` crashed with "'NoneType' object is not iterable". An omitted list now means "keep the current links"; a sent list is reconciled (and must still meet the end's minimum). Found by driving all 102 routes of the hotel model.
…in the browser Driving the generated React app against the hotel model surfaced four defects the API tests could not see: - react: the table only fetched ?detailed=true when a displayed column was a lookup, but the edit form prefills relationship selections from the detailed row; a table without a relationship column opened the form with every relationship empty and a save detached them all. TableBlock and TableComponent now fetch detailed whenever the form has a lookup, and a relationship the row never carried is left out of the save if untouched. - react: `options.columns ?? []` produced a new array every render, which invalidated the columns -> formColumns memo chain and made the modal effects re-run on every render: an open Add/Edit dialog fired ~1000 lookup requests per second. Stable fallback constants fix the loop. - react: a refused row delete (e.g. the 409 for the last guest of a booking) was only console.error'd; the table now shows the backend's message in a dismissible banner. Auto-derived columns skip relationship payloads instead of rendering them as raw JSON. - backend: PUT only assigned the class's own attributes, so editing an inherited one (Employee.last_name, defined on Person) was silently ignored; ancestors' attributes are now updated too. Verified in Chrome on the generated app: employee rename with its booking kept, guest/link deletes refused with the reason shown, room edit by its real primary key, booking delete cascading its links.
…tiplicity enforcement in generated apps - Bump setup.cfg 7.14.1 -> 7.14.2 and add the v7.14.2 release notes. - Backend: modular per-file FastAPI generator (#597), association-class cascade delete and real row keys (#598), lower-bound multiplicity guards, omitted-list PUT, inherited-attribute PUT, and the generated dialog fixes (detailed prefill, request loop, delete-error banner). - Frontend submodule unchanged: develop and main are already in sync.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Patch release: a modular per-resource FastAPI backend, and correct deletes, edits and multiplicities in generated web applications. Everything was verified end to end on a hotel-booking model: every generated API route was driven with real data, and the generated React screens were exercised in a browser. No metamodel or API-contract changes — freshly generated applications keep the same REST contract.
Highlights
BackendGeneratornow emits a slimmain_api.pythat wires onerouters/<class>.pyper resource, plusdatabase.py(shared engine/session honoringDATABASE_URL) andbal_stdlib.py.RESTAPIGenerator(backend=True)renders the same API layer and the monolithic template is gone, so the association-class, OCL and method-normalization logic lives in one place. The route table is identical to the previous generator on the reference model. Fixed along the way:/search/endpoints expose real filter parameters (including inherited attributes), path parameters use the declared PK type, a modeled method without an implementation answers501instead of a fake success,/healthreally queries the database, andsql_alchemy.pysharesDATABASE_URL, no longer creates tables on import, and types association-class FKs after the referenced PK.BookingwithReservedRoomrows crashed the generated backend (the link's FK is part of its composite PK). The entity → links relationships now cascade (all, delete-orphan) from both ends, andDELETEreturns a snapshot of the deleted row. The generated table addresses rows by their real key (row_key_fields): aRoombynumber, a link by both FKs (/reservedroom/{booking_id}/{rooms_id}/) — it used to guess the first column of the row.409naming the entity and the minimum ("Cannot delete Guest 3: Booking 10 requires at least 1 guests"), and deleting the target of a mandatory to-one end gets the same message instead of aNOT NULLcrash. Editing the owner of a one-to-many end (Employee.manages) now diffs members instead of nulling every FK first, which had made such entities impossible to edit.PUT; and aPUTthat omits a many-to-many list leaves the links untouched.