feat: scoped-routing resolution via resolve_route() in api middleware (Lane B)#1398
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Routing boundary research — findings & recommendation for #1398TL;DRThe two-projection model is right, and it's already half-built: k8s Ingress objects are already generated from the DB by a reconciler (they are not hand-managed), and Traefik today matches host only and funnels everything to one gateway Service. The server's job is, and should remain, host → tenant (api/db/schemas/role) — which Traefik cannot do. #1398 is directionally correct as the Layer-2 consumer but is currently too wide (it feeds real 1. Current request path (end to end)
2. Is ingress DB-generated? YESLayer 1 is already a projection of DB rows: Moreover, the scoped-routing migration plan already prescribes the two-projection cutover: "Read cutover — the GraphQL server switches host resolution … to 3. The correct boundarySame DB rows, two projections:
So Dan's "dumb Deployment" model holds: the server matches an api by host and renders it. The legit-sounding case for in-server path routing (website-builder 4. What's wrong with #1398 as-is (concrete)
5. Recommendation for #1398: narrow, then merge dormantDo not close; do not merge as-is. Keep the PR and make one small change before merge:
Follow-ups (separate PRs, per the migration plan):
|
Plan: narrow to host-only resolution (per the routing-boundary decision)Traefik/Ingress owns host→workload and L7 path/method routing; the server is a dumb deployment that only needs host→tenant/api/db/role. So:
No cutover; this just aligns the dormant scoped path with the Traefik boundary before enabling. |
…g stays in Traefik)
Summary
Lane B of constructive-planning#1214: the GraphQL server can resolve incoming hostnames through the compiled scoped-routing plane shipped in constructive-db (now the typed per-type catalog, constructive-db#2345), via the frozen DB↔server contract.
Host-only projection (per the routing boundary decision): Traefik/Ingress owns host→workload and L7 path/method routing; the server is a dumb Deployment that only maps host → tenant/api/db/role. So the server always calls the frozen SQL contract with the root path and no method:
The
resolve_route()function signature and return shape are unchanged — only how the server calls it. Server-side path/method matching is dropped from the scoped path (it was also inconsistent with the host-wide svc cache key).Additive and opt-in — no behavior change by default:
ApiOptions.enableScopedRouting(defaultfalse) andApiOptions.scopedRoutingSchema(defaultconstructive_routing_public) in@constructive-io/graphql-types.middleware/routing.ts:resolveRoute(pool, schema, host)(typedResolvedRoutematching the contract row; treats the contract no-match rowroute_binding_id IS NULL, missing resolver42883, or missing schema3F000as "no match") androuteToApiStructure()(maps an api-target route'sresolved_config—{dbname, role_name, anon_role, schemas, api_id, database_id, is_public}— onto the existingApiStructure).getApiConfigdomain-lookup mode:resolveScopedRoute(ctx) ?? resolveDomainLookup(ctx)— scoped plane first, then the legacyservices_public.domainsquery remains the authoritative fallback. Module settings (RLS/auth/CORS/…) still resolve through the existing loader registry.Full read-cutover (scoped plane authoritative, legacy queries removed) is a later phase per the migration plan in constructive-db
docs/architecture/scoped-domain-routing.md.Unit tests in
routing.test.tscover host-only invocation (resolve_route($1, '/', NULL)with[host]only), the contract no-match row, resolver-not-installed fallback, error rethrow, schema-name validation, ApiStructure mapping, and scoped-first/legacy-fallback resolution order ingetApiConfig.Link to Devin session: https://app.devin.ai/sessions/9a83c1e46fc744c487980aee5844f556
Requested by: @pyramation