Skip to content

v7.14.2 - #599

Merged
ArmenSl merged 9 commits into
masterfrom
development
Sep 9, 2026
Merged

v7.14.2#599
ArmenSl merged 9 commits into
masterfrom
development

Conversation

@ArmenSl

@ArmenSl ArmenSl commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

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

  • The generated backend is modular: BackendGenerator now emits a slim main_api.py that wires one routers/<class>.py per resource, plus database.py (shared engine/session honoring DATABASE_URL) and bal_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 answers 501 instead of a fake success, /health really queries the database, and sql_alchemy.py shares DATABASE_URL, no longer creates tables on import, and types association-class FKs after the referenced PK.
  • Deleting an entity deletes its association-class links: deleting a Booking with ReservedRoom rows 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, and DELETE returns a snapshot of the deleted row. The generated table addresses rows by their real key (row_key_fields): a Room by number, a link by both FKs (/reservedroom/{booking_id}/{rooms_id}/) — it used to guess the first column of the row.
  • Lower-bound multiplicities are enforced wherever a relationship can shrink: deleting an entity, removing a link, or editing a relationship list is refused with a 409 naming 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 a NOT NULL crash. 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.
  • Generated edit dialogs are safe and fast: the table fetches detailed rows whenever its form has a relationship, so the edit dialog opens with the current selections (a save used to silently detach every relationship); relationships the row never carried are left out of the save; an open dialog no longer fires ~1000 lookup requests per second; refused deletes show the backend's reason in a banner; inherited attributes are updated by PUT; and a PUT that omits a many-to-many list leaves the links untouched.

ArmenSl and others added 9 commits September 9, 2026 11:08
…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.
@ArmenSl
ArmenSl merged commit 823087e into master Sep 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant