Skip to content

feat: API-bound function invocation — REST /fn routes + GraphQL bindings plugin (consolidates #1364 + #1365)#1369

Open
pyramation wants to merge 7 commits into
mainfrom
feat/api-function-invocation
Open

feat: API-bound function invocation — REST /fn routes + GraphQL bindings plugin (consolidates #1364 + #1365)#1369
pyramation wants to merge 7 commits into
mainfrom
feat/api-function-invocation

Conversation

@pyramation

@pyramation pyramation commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates #1364 (express-context jwt.claims.api_id provenance + REST /fn/:alias routes) and #1365 (graphile plugin exposing API-bound functions as GraphQL mutations) into one branch, then de-duplicates the overlap and makes the whole path scope-aware. Supersedes both PRs.

Both protocols share one binding model end to end: hostname → services_public.domainsapi_id → trusted jwt.claims.api_id claim → same function_api_bindings row (protocol enablement in config: { "graphql": true, "rest": { "path", "methods" } }) → plain RLS-enforced INSERT into that scope's function_invocations. No SECURITY DEFINER, no new tables; ajv payload validation intentionally deferred.

Scope-aware module resolution

An API is a global surface; a database may have function modules in multiple scopes, and bindings can live in any of them. The express-context compute loader now returns all function modules for the database (from metaschema_modules_public.function_module + function_invocation_module — no LIMIT 1, no pg_catalog discovery, no hard-coded names):

interface ComputeConfig { modules: ComputeModuleConfig[] }  // was a single module
  • REST POST /fn/:alias searches every module's bindings table for (api_id, alias); exactly one match proceeds, zero → 404, cross-scope collision → 409. The INSERT targets the matched module's own invocations table. GET /fn/invocations/:id searches each distinct invocations table.
  • The graphile plugin takes a modules: ComputeModuleNames[] array, loads bindings from every scope, and emits one mutation per graphql-enabled binding, inserting into that binding's module invocations table. It emits only mutations — no API→bindings reverse relation is added to the schema.
  • Lookups are strict: required loader/plugin options throw when missing; 42P01 on a module's tables is treated as "module not provisioned" (loader returns undefined, routes 404); all other loader errors propagate.

Cleanup on top of the raw merge

  • Use existing libraries: inflekt (toCamelCase/toPascalCase/toConstantCase) for mutation/type/enum naming; escapeIdentifier from pg replaces hand-rolled quoting in the plugin and graphql/server/src/middleware/fn.ts.
  • Fixture dedup: removed the plugin's private fn_test SQL; plugin tests seed the shared __fixtures__/seed/{services,compute} fixtures (extended for payload_args derivation, JSON-Schema enum/required, JSON fallback, graphql-disabled, other-API filtering, REST enabled/disabled), so REST and GraphQL tests run against identical data including the function_invocations_api_insert WITH CHECK provenance policy.
  • Lockfile: regenerated from main, reducing the feat(graphile-function-bindings): expose API-bound functions as GraphQL mutations #1365 churn from ~10k lines to +50.

Testing: targeted builds green; plugin derive + schema tests, express-context (4/4), and graphql/server-test/__tests__/fn-routes.integration.test.ts (7/7) pass locally. One plugin test (resize.invocation sub-select) hits a pre-existing local-env pgSelect failure (reading 'items') that also breaks untouched graphile-test suites locally — relying on CI (which runs the plugin suite in the pg-tests matrix) for that path.

Link to Devin session: https://app.devin.ai/sessions/3ca7b78eeff8401d9108bb174d8eb3eb
Requested by: @pyramation

pyramation and others added 6 commits July 12, 2026 13:55
- Set jwt.claims.api_id transaction-locally via pgSettings for every
  request on an API surface, derived from the server-side hostname ->
  services_public.domains -> api_id resolution (never client input)
- Add compute module loader (function/invocation module metadata)
- Add POST /fn/:alias and GET /fn/invocations/:id REST routes resolving
  function_api_bindings by (api_id, alias); 404 on missing binding,
  rest-disabled config, or disallowed method; RLS enforced end to end
- ajv/JSON-Schema payload validation intentionally deferred (TODO hook)

Refs constructive-io/constructive-planning#1149
…QL mutations

One mutation per graphql-enabled function_api_bindings row for the API,
with input types derived from JSON Schema / payload_args (JSON fallback)
and a plain RLS-enforced insert into function_invocations.

Track B2 of constructive-io/constructive-planning#1149
@pyramation pyramation self-assigned this Jul 12, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

- compute loader returns all function modules for the database (no LIMIT 1)
- REST /fn/:alias searches every module's bindings table; 409 on cross-scope
  alias collisions; invocation GET searches each distinct invocations table
- graphile plugin takes a modules array and emits mutations from every
  scope's bindings, inserting into that module's invocations table
- loader treats undefined_table (42P01) as module-not-provisioned; all other
  loader errors propagate
- mark the mutation insert step as a side effect for grafast
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