graphql-generic connector¶
Ingest from any GraphQL API by putting the query directly in the manifest. GraphQL is a
request shape on the universal REST engine (kind: graphql), not a separate connector —
it reuses auth, rate-limiting, retry, hybrid JSONB+typed storage, and idempotent upsert.
YAML manifest¶
# 10-relay-basic.yaml — GraphQL source over the rest-generic engine: the query lives
# in the manifest, relay pagination re-injects endCursor into the variables.
#
# Apply and run:
# lm apply -f docs/connectors/graphql/10-relay-basic.yaml
# lm connector run shop
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata: { name: shop, connector_type: rest-generic, tags: [commerce] }
spec:
base_url: https://myshop.example.com
graphql_path: /api/graphql # the single POST endpoint (default: /graphql)
auth: { kind: bearer, token_env: SHOP_TOKEN }
headers: # applied to every request (static or env)
- { name: X-Api-Version, value: "2024-10" }
- { name: X-Tenant, value_env: SHOP_TENANT_HDR }
default_rate_limit: 50/s
endpoints:
- id: orders # endpoint identifier (kebab-case)
kind: graphql
query: |
query Orders($after: String) {
orders(first: 100, after: $after) {
edges { node { id name totalPrice updatedAt } }
pageInfo { hasNextPage endCursor }
}
}
variables: { someFlag: true } # static variables merged into every request
pagination:
kind: relay
cursor_binding: variable # cursor goes into variables, not the URL
cursor_var: after
cursor_path: $.data.orders.pageInfo.endCursor
has_more_path: $.data.orders.pageInfo.hasNextPage
response_path: $.data.orders.edges[*].node
incremental: { mode: full }
target:
kind: hybrid
table: ext.shop_orders
unique_key: $.id
promote:
- { name: total, path: $.totalPrice, type: decimal }
- { name: updated_at, path: $.updatedAt, type: timestamptz }
Fields¶
| Field | Where | Required | Notes |
|---|---|---|---|
graphql_path |
spec |
no | The single POST endpoint; requests go to base_url + graphql_path (default /graphql) |
kind: graphql |
endpoint | no | Marker only — an endpoint carrying a query is treated as GraphQL either way |
query |
endpoint | yes | The GraphQL query document; declare the cursor variable it expects (e.g. $after) |
variables |
endpoint | no | Static variables sent with every request; the relay cursor is injected on top |
pagination |
endpoint | yes | Hard rule: kind: relay with cursor_binding: variable — anything else is refused at apply |
cursor_var |
pagination | yes | The variable that receives endCursor (e.g. after) |
cursor_path, has_more_path, response_path |
pagination | yes | JSONPaths to endCursor, hasNextPage, and the node list |
target |
endpoint | yes | Same hybrid target as REST: table, unique_key, promote |
path and method are implicit — the request is always a POST to graphql_path — and not
required. Conversely, cursor_binding: variable is only valid here: on a non-graphql endpoint
the validator refuses it (the cursor would ride a POST body that a plain GET never sends).
How it works¶
- The query is POSTed as
{ "query": ..., "variables": ... }tobase_url + graphql_path. - Relay pagination reads
pageInfo { hasNextPage endCursor }; theendCursoris re-injected intovariables.<cursor_var>for the next page (Connections spec). response_path: ...edges[*].nodeextracts the clean node list (Jayway JSONPath projection).- GraphQL errors arrive as HTTP 200 with an
errors[]array — any error fails the run and dead-letters the chunk (strict; no partial ingestion in this release). A 200 envelope withdata: nulland no errors is refused the same way — success MUST carry data.
Headers¶
spec.headers[] applies to every request: { name, value } (static) or { name, value_env }
(resolved from the environment / a secret). Auth-injected headers win on a name clash.
Not yet supported (future)¶
Mutations, subscriptions, schema-introspection discovery, persisted queries, query batching,
per-endpoint header overrides, and on_error: warn (partial-success ingestion).