-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sdk): add defineHttpAdapter for declarative gateway HTTP adapters #1173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Mistat
wants to merge
5
commits into
main
Choose a base branch
from
feat/http-adapter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
4d8010d
feat(sdk): add defineHttpAdapter for declarative gateway HTTP adapters
Mistat 083167d
fix(sdk): wire defineHttpAdapter into existing app/deploy modules
Mistat 967374c
feat(example): add HTTP adapter samples returning XML
Mistat 2a39c26
Merge remote-tracking branch 'origin/main' into feat/http-adapter
toiroakr 96f6cb8
fix(sdk): address Copilot review on http-adapter PR
Mistat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@tailor-platform/sdk": minor | ||
| --- | ||
|
|
||
| Add `defineHttpAdapter()` for declaring HTTP adapters that translate HTTP requests into GraphQL queries and reshape the responses. Adapter files are discovered via the new `httpAdapter.files` glob in `defineConfig()`. At deploy time the `input`/`output` functions are bundled and embedded as gateway filters on the application. Requires the per-workspace feature flag `20260413_platform_filter_router` to be enabled before adapter routes serve traffic. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { defineHttpAdapter } from "@tailor-platform/sdk"; | ||
|
|
||
| function escapeXml(value: unknown): string { | ||
| if (value === null || value === undefined) return ""; | ||
| const str = | ||
| typeof value === "string" || typeof value === "number" || typeof value === "boolean" | ||
| ? String(value) | ||
| : JSON.stringify(value); | ||
| return str | ||
| .replace(/&/g, "&") | ||
| .replace(/</g, "<") | ||
| .replace(/>/g, ">") | ||
| .replace(/"/g, """) | ||
| .replace(/'/g, "'"); | ||
| } | ||
|
|
||
| export default defineHttpAdapter({ | ||
| name: "get-user", | ||
| pathPattern: "/users/*", | ||
| methods: ["GET"], | ||
| input: (req) => { | ||
| const segments = req.path.split("/").filter(Boolean); | ||
| const id = segments[segments.length - 1] ?? ""; | ||
| return { | ||
| query: `query GetUser($id: ID!) { | ||
| user(id: $id) { | ||
| id | ||
| name | ||
| role | ||
| status | ||
| } | ||
| }`, | ||
| variables: { id }, | ||
| }; | ||
| }, | ||
| output: (resp) => { | ||
| const data = resp.data as { user: Record<string, unknown> | null } | null | undefined; | ||
| const user = data?.user; | ||
| if (!user) { | ||
| return { | ||
| statusCode: 404, | ||
| headers: { "content-type": "application/xml; charset=utf-8" }, | ||
| body: `<?xml version="1.0" encoding="UTF-8"?>\n<error>user not found</error>`, | ||
| }; | ||
| } | ||
| const xml = | ||
| `<?xml version="1.0" encoding="UTF-8"?>\n` + | ||
| `<user>` + | ||
| `<id>${escapeXml(user.id)}</id>` + | ||
| `<name>${escapeXml(user.name)}</name>` + | ||
| `<email>${escapeXml(user.email)}</email>` + | ||
| `<role>${escapeXml(user.role)}</role>` + | ||
| `<status>${escapeXml(user.status)}</status>` + | ||
| `</user>`; | ||
| return { | ||
| statusCode: 200, | ||
| headers: { "content-type": "application/xml; charset=utf-8" }, | ||
| body: xml, | ||
| }; | ||
| }, | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { defineHttpAdapter } from "@tailor-platform/sdk"; | ||
|
|
||
| function escapeXml(value: unknown): string { | ||
| if (value === null || value === undefined) return ""; | ||
| const str = | ||
| typeof value === "string" || typeof value === "number" || typeof value === "boolean" | ||
| ? String(value) | ||
| : JSON.stringify(value); | ||
| return str | ||
| .replace(/&/g, "&") | ||
| .replace(/</g, "<") | ||
| .replace(/>/g, ">") | ||
| .replace(/"/g, """) | ||
| .replace(/'/g, "'"); | ||
| } | ||
|
|
||
| function actorXml(tag: string, actor: Record<string, unknown> | undefined): string { | ||
| if (!actor) return `<${tag} />`; | ||
| return ( | ||
| `<${tag}>` + | ||
| `<id>${escapeXml(actor.id)}</id>` + | ||
| `<type>${escapeXml(actor.type)}</type>` + | ||
| `<role>${escapeXml(actor.role)}</role>` + | ||
| `</${tag}>` | ||
| ); | ||
| } | ||
|
|
||
| export default defineHttpAdapter({ | ||
| name: "whoami", | ||
| pathPattern: "/whoami", | ||
| methods: ["GET"], | ||
| input: () => ({ | ||
| query: `query Whoami { | ||
| showUserInfo { | ||
| user { | ||
| id | ||
| type | ||
| role | ||
| } | ||
| invoker { | ||
| id | ||
| type | ||
| role | ||
| } | ||
| } | ||
| }`, | ||
| }), | ||
| output: (resp) => { | ||
| const data = resp.data as | ||
| | { | ||
| showUserInfo?: { | ||
| user?: Record<string, unknown>; | ||
| invoker?: Record<string, unknown>; | ||
| }; | ||
| } | ||
| | null | ||
| | undefined; | ||
| const info = data?.showUserInfo; | ||
| const xml = | ||
| `<?xml version="1.0" encoding="UTF-8"?>\n` + | ||
| `<whoami>` + | ||
| actorXml("user", info?.user) + | ||
| actorXml("invoker", info?.invoker) + | ||
| `</whoami>`; | ||
| return { | ||
| statusCode: 200, | ||
| headers: { "content-type": "application/xml; charset=utf-8" }, | ||
| body: xml, | ||
| }; | ||
| }, | ||
| }); |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # HTTP Adapter | ||
|
|
||
| HTTP adapters expose REST-style HTTP endpoints on your application's gateway by translating each request into a GraphQL query and (optionally) reshaping the GraphQL response back into an HTTP response. | ||
|
|
||
| ## Overview | ||
|
|
||
| Each HTTP adapter is a single file that declares: | ||
|
|
||
| - The HTTP `pathPattern` and `methods` it handles | ||
| - An `input` function that converts an incoming HTTP request into a GraphQL request (`query`, `variables`, `operationName`) | ||
| - An optional `output` function that converts the GraphQL response into an HTTP response (`statusCode`, `headers`, `body`) | ||
|
|
||
| At deploy time the SDK bundles each `input` and `output` function into a standalone JS script that runs in the gateway's sandboxed runtime when a matching request hits `/f/<pathPattern>`. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Each adapter file must call `defineHttpAdapter` exactly once and `export default` the result | ||
| - `name` must be a string literal that matches `^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$` and be unique across all adapters | ||
| - `input` and `output` must be inline arrow or `function` expressions (not references to functions defined elsewhere) | ||
| - `input` and `output` **must be synchronous** — `async`/`await` and top-level `await` are not supported by the gateway runtime | ||
|
|
||
| ## Runtime Constraints | ||
|
|
||
| Adapter scripts are bundled to an ES2017 IIFE and executed in the gateway's sandboxed Sobek runtime. The following are **not** available: | ||
|
|
||
| - Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) — rejected at build time | ||
| - `async`/`await` and top-level `await` — rejected at build time | ||
| - `fetch`, `setTimeout`, `setInterval`, and other browser/host globals | ||
| - Any third-party libraries that depend on the above | ||
|
Mistat marked this conversation as resolved.
|
||
|
|
||
| Each bundled script is capped at 256 KB (with a warning at 64 KB). | ||
|
|
||
| ## Activation | ||
|
|
||
| HTTP adapters are gated by a per-workspace feature flag (`20260413_platform_filter_router`). Until the flag is enabled for a workspace, requests to `/f/<path>` return `404`. Contact your platform admin to enable adapters in your environment. | ||
|
|
||
| ## Configuration | ||
|
|
||
| Add an `httpAdapter` entry to `defineConfig`: | ||
|
|
||
| ```typescript | ||
| // tailor.config.ts | ||
| import { defineConfig } from "@tailor-platform/sdk"; | ||
|
|
||
| export default defineConfig({ | ||
| name: "my-app", | ||
| httpAdapter: { | ||
| files: ["adapters/**/*.ts"], | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## Defining an Adapter | ||
|
|
||
| ```typescript | ||
| // adapters/get-user.ts | ||
| import { defineHttpAdapter } from "@tailor-platform/sdk"; | ||
|
|
||
| export default defineHttpAdapter({ | ||
| name: "get-user", | ||
| pathPattern: "/users/*", | ||
| methods: ["GET"], | ||
| input: (req) => ({ | ||
| query: `query GetUser($id: ID!) { user(id: $id) { id name email } }`, | ||
| variables: { id: req.path.split("/")[2] }, | ||
| }), | ||
| output: (resp) => ({ | ||
| statusCode: 200, | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify(resp.data?.user ?? null), | ||
| }), | ||
| }); | ||
| ``` | ||
|
|
||
| A request to `GET /f/users/abc-123` will invoke `input(req)`, execute the resulting GraphQL query against your application's GraphQL endpoint (with the caller's auth context preserved), then invoke `output(resp)` to produce the HTTP response. | ||
|
|
||
| If `output` is omitted, the raw GraphQL response is returned as JSON. | ||
|
|
||
| ## Path Pattern | ||
|
|
||
| - Literal segments must match exactly: `/users/list` matches only `/users/list` | ||
| - A `*` in the middle matches exactly one segment: `/api/*/users` matches `/api/v1/users` | ||
| - A trailing `*` matches all remaining segments: `/api/*` matches `/api/v1/users/123` | ||
|
|
||
| ## Type Reference | ||
|
|
||
| ```typescript | ||
| type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD"; | ||
|
|
||
| type HttpAdapterRequest = { | ||
| method: HttpMethod; | ||
| path: string; | ||
| headers: Record<string, string>; | ||
| query: Record<string, string>; | ||
| body: string; | ||
| }; | ||
|
|
||
| type HttpAdapterInputResult = { | ||
| query: string; | ||
| variables?: Record<string, unknown>; | ||
| operationName?: string; | ||
| }; | ||
|
|
||
| type HttpAdapterGraphQLResponse = { | ||
| data?: unknown; | ||
| errors?: unknown; | ||
| extensions?: unknown; | ||
| }; | ||
|
|
||
| type HttpAdapterOutputResult = { | ||
| statusCode?: number; | ||
| headers?: Record<string, string>; | ||
| body: string; | ||
| }; | ||
| ``` | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean it is running in a runtime that is different from both the tailordb hook/validate runtime and the function runtime?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes — HTTP adapter scripts run in a separate runtime from both the TailorDB hook/validate runtime and the function runtime. It is a Go-based JavaScript runtime (Sobek), and there are restrictions on imports and host APIs as a result.