"""routes_connected_tables.py — the SOURCE-NEUTRAL door to a connected grid (W31-T49, contract C4). `GET /api/v1/connected-tables/{table_key}/rows` — one window over any grid whose rows come from a connector and are read THROUGH the per-tenant DuckDB mirror rather than copied into the tenant document. Odoo's four locked databases and its two line grains today; Meta Ads' entity and Insights grids when `meta_relational` registers its specs (R2). ⭐ WHY THIS FILE EXISTS. The read-through window shipped in wave 30 inside `routes_odoo_tables.py`, Odoo-named down to the URL — `/api/v1/odoo-tables/{key}/rows`. R2 puts a SECOND connector on the same mirror, and a Meta campaign served from a URL with `odoo` in it is a name that lies to every reader of a network tab, a log line and a bug report. So the capability gets a name that describes what it IS. ⛔⛔ IT IS AN ALIAS, NOT A REIMPLEMENTATION, AND THAT IS THE WHOLE DESIGN. T49's `done-when` is *"the Odoo path behaves byte-identically, proven by an NC"*. There are two ways to make that true: write a second handler and test that the two agree, or have ONE handler behind two URLs. Only the second is true BY CONSTRUCTION — the first is true on the day it is written and drifts the first time somebody fixes a bug in one of them. Every request here lands in `routes_odoo_tables.odoo_table_rows`, unchanged, so the two doors cannot disagree without a line being deleted. [[one-question-two-normalizers]] ⚠ THE OLD URL STAYS, and that is not politeness to a client we control. `CustomerGrid`'s bridge, saved views and anything an operator has in a browser tab all name it; retiring it in the same wave that introduces the new one would turn a rename into an outage for a feature that is already half-proven (D-174). The Odoo-named route is kept as a deliberate alias with a stated retirement condition rather than as a leftover: it goes when nothing in `web/src` names it. ⛔ THE MOUNT IS THE POINT AND IT HAS BEEN MISSED BEFORE — three finished routers shipped 404-dead behind green gates in wave 23, and wave 28 proved a MOUNTED router can still be uncallable (`session.is_admin`, an attribute that does not exist, raising above the route's own `try`). So `verify_meta` both enumerates `main.app.routes` for this path AND calls it. """ from fastapi import APIRouter, Depends, Query from deps import Session, require_session router = APIRouter(prefix="/api/v1") @router.get("/connected-tables/{table_key}/rows") def connected_table_rows(table_key: str, offset: int = Query(default=0, ge=0), limit: int = Query(default=0), filters: str = Query(default=None), filterConj: str = Query(default="and"), sorts: str = Query(default=None), search: str = Query(default=None), session: Session = Depends(require_session)): """ONE WINDOW over a connected grid — `{fields, rows, total, totalUnfiltered, offset, limit, limits, identity, recordsMutable}`, whichever connector the rows come from. ⛔ EVERY GUARANTEE IS INHERITED, NOT RESTATED, because the body below is one call. `total` is a `SELECT count(*)` and never `len(rows)`; the filter, sort and search resolve in SQL over the whole table; anything that cannot be answered is reported in `limits` with a cause and a recommendation (R6's second sentence); the permission wall is `_defn_or_refuse`; and the cross-tenant mirror guard (D-169) refuses with `409 cross_tenant_store` before a cursor is handed out. Documenting them again HERE would be a second statement of a contract that has one home — and the day the two drifted, this file would be the one that lied. """ import routes_odoo_tables return routes_odoo_tables.odoo_table_rows( table_key, offset=offset, limit=limit, filters=filters, filterConj=filterConj, sorts=sorts, search=search, session=session)