Documentation
¶
Overview ¶
Package openapi consumes OpenAPI 3 specifications dropped into a config directory and exposes their GET operations as remote-joinable fields and top-level virtual tables. It is the upstream-API counterpart to GraphJin's existing remote_api resolver and is method-agnostic at the HTTP/auth layer so write-side support can be layered on later without restructuring.
Index ¶
- func SynthesiseArgs(schema, table string, op OpDescriptor) []sdata.DBColumn
- func SynthesiseColumns(schema, table string, ref *openapi3.SchemaRef, resultPath []string) []sdata.DBColumn
- type AuthConfig
- type AuthProvider
- type CallParams
- type Caller
- type ConcurrencyConfig
- type JoinConfig
- type LoadResult
- type LoaderOptions
- type OpDescriptor
- type OpMode
- type OperationOverride
- type ParamLocation
- type ParamSpec
- type Registry
- type Runtime
- type Spec
- type SpecConfig
- type SpecRuntime
- type TokenExchangeRequest
- type TokenExchangeResponse
- type TokenFromRequest
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SynthesiseArgs ¶
func SynthesiseArgs(schema, table string, op OpDescriptor) []sdata.DBColumn
SynthesiseArgs returns DBColumn entries for an operation's path and query parameters. The bridge sets these on DBTable.Args; intro.go emits them as field arguments.
func SynthesiseColumns ¶
func SynthesiseColumns(schema, table string, ref *openapi3.SchemaRef, resultPath []string) []sdata.DBColumn
SynthesiseColumns returns DBColumn entries for the response payload's top-level fields, after stripping any result-path wrapper. Returns nil when the schema isn't an object we can introspect (e.g. raw scalar response, schema absent).
Types ¶
type AuthConfig ¶
type AuthConfig struct {
// Scheme: bearer | basic | api_key | oauth2_client_credentials | token_exchange
Scheme string `mapstructure:"scheme" json:"scheme" yaml:"scheme"`
// Bearer: a static or env-supplied token. Use TokenFromRequest for
// per-request tokens forwarded from the incoming GraphJin request.
Token string `mapstructure:"token" json:"token" yaml:"token"`
TokenFromRequest *TokenFromRequest `mapstructure:"token_from_request" json:"token_from_request" yaml:"token_from_request"`
// Basic auth.
Username string `mapstructure:"username" json:"username" yaml:"username"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
// API key (header or query).
KeyName string `mapstructure:"key_name" json:"key_name" yaml:"key_name"`
KeyValue string `mapstructure:"key_value" json:"key_value" yaml:"key_value"`
KeyIn string `mapstructure:"key_in" json:"key_in" yaml:"key_in"` // "header" | "query"
// OAuth2 client_credentials grant.
TokenURL string `mapstructure:"token_url" json:"token_url" yaml:"token_url"`
ClientID string `mapstructure:"client_id" json:"client_id" yaml:"client_id"`
ClientSecret string `mapstructure:"client_secret" json:"client_secret" yaml:"client_secret"`
Scopes []string `mapstructure:"scopes" json:"scopes" yaml:"scopes"`
// token_exchange — generic "POST credentials → JSON with token" flow.
// Covers Salesforce Marketing Cloud Personalization and similar
// vendor-specific token endpoints that aren't standard OAuth2.
Request *TokenExchangeRequest `mapstructure:"request" json:"request" yaml:"request"`
Response *TokenExchangeResponse `mapstructure:"response" json:"response" yaml:"response"`
// How the resolved token attaches to outgoing operation requests.
AttachAs string `mapstructure:"attach_as" json:"attach_as" yaml:"attach_as"` // "header" | "query"
AttachName string `mapstructure:"attach_name" json:"attach_name" yaml:"attach_name"` // e.g. "Authorization"
AttachFormat string `mapstructure:"attach_format" json:"attach_format" yaml:"attach_format"` // e.g. "Bearer {token}"
// CacheTTL overrides the token cache TTL when the auth response
// doesn't include an expires_in field. Format: Go duration ("3500s").
CacheTTL string `mapstructure:"cache_ttl" json:"cache_ttl" yaml:"cache_ttl"`
}
AuthConfig describes how outgoing requests to the upstream are authenticated. Scheme selects which set of fields is meaningful — the others are ignored.
All credential-bearing string fields support GraphJin's standard ${VAR} env-var expansion at load time.
type AuthProvider ¶
type AuthProvider interface {
Apply(ctx context.Context, req *http.Request, hdrIn http.Header) error
}
AuthProvider attaches authentication to outgoing requests to an upstream API. Implementations are constructed once per spec at boot time and reused across every operation against that spec.
Apply mutates req in place. The hdrIn parameter carries headers from the *incoming* GraphJin request, enabling per-tenant pass-through patterns where the end-user's credentials are forwarded upstream rather than a single service credential being shared across tenants.
OnUnauthorized is invoked by the resolver after a 401 response so providers that cache tokens can invalidate them and the resolver can retry once. Stateless providers (bearer with literal token, basic auth) implement this as a no-op.
func NewAuthProvider ¶
func NewAuthProvider(cfg AuthConfig, httpClient *http.Client) (AuthProvider, error)
NewAuthProvider builds the right AuthProvider for cfg.Scheme. An empty or unrecognised scheme returns the no-op provider — operations against the spec will simply send unauthenticated requests, which is the correct behaviour for public APIs.
The returned provider holds httpClient when it needs to make its own requests (token exchange, oauth2 client_credentials). httpClient must not be nil for those schemes.
type CallParams ¶
type CallParams struct {
PathValues map[string]string
QueryValues map[string]string
HeaderValues map[string]string
IncomingHeaders http.Header
}
CallParams carries the per-call inputs. PathValues populates the URL template's {placeholders}; QueryValues and HeaderValues populate non-path parameters (query strings and HTTP headers respectively); IncomingHeaders is the inbound GraphJin request's header set, used only for pass-through auth and ignored otherwise.
type Caller ¶
type Caller struct {
// contains filtered or unexported fields
}
Caller executes a single OpenAPI operation. One Caller is constructed per operation at boot time and reused for every call to that operation. The construction cost (URL parsing, escape-table init) is amortised across the request lifetime.
Callers are safe for concurrent use — the auth provider, limiter, and http client all guard their own state. The Caller itself only reads from its fields after construction.
func NewCaller ¶
func NewCaller(op *OpDescriptor, baseURL string, auth AuthProvider, lim *limiter, httpClient *http.Client) *Caller
NewCaller wires up everything a single operation needs to execute. httpClient must be non-nil; auth and limiter must already be initialised by the SpecRuntime that holds them.
type ConcurrencyConfig ¶
type ConcurrencyConfig struct {
MaxConcurrent int `mapstructure:"max_concurrent" json:"max_concurrent" yaml:"max_concurrent"`
RateLimitPerSec int `mapstructure:"rate_limit_per_second" json:"rate_limit_per_second" yaml:"rate_limit_per_second"`
}
ConcurrencyConfig bounds outgoing-request fan-out per spec.
MaxConcurrent caps simultaneous in-flight requests; RateLimitPerSec is a token-bucket limit. Either can be zero to disable. Sensible defaults are applied at construction time when unset.
type JoinConfig ¶
type JoinConfig struct {
ParentTable string `mapstructure:"parent_table" json:"parent_table" yaml:"parent_table"`
ParentColumn string `mapstructure:"parent_column" json:"parent_column" yaml:"parent_column"`
Param string `mapstructure:"param" json:"param" yaml:"param"` // OpenAPI param name receiving the column value
ExposeAs string `mapstructure:"expose_as" json:"expose_as" yaml:"expose_as"` // GraphQL field name on the parent
}
JoinConfig wires an operation to a DB table+column. With this set, the operation is exposed as a child field on the parent table; without it, the operation is still queryable as a top-level field with an explicit argument for the same parameter.
type LoadResult ¶
LoadResult bundles the registry produced by a Load call along with any non-fatal warnings (per-spec parse errors, per-operation classification reasons) that the caller can surface in startup logs. A nil Registry indicates the directory was missing or empty — that is not an error, it just means OpenAPI integration is dormant for this deployment.
func Load ¶
func Load(opts LoaderOptions, configs map[string]SpecConfig, logger *log.Logger) (*LoadResult, error)
Load discovers OpenAPI specs in opts.SpecsDir, parses each one, applies per-spec user configuration, and classifies every operation. It is designed to be tolerant: a bad spec produces a warning and is dropped from the registry, but never aborts loading other specs. Per-operation classification failures are likewise recorded and surfaced via warnings.
The configs map is keyed by spec key (filename without extension). Only specs whose key matches a configured entry get their AuthConfig populated; specs without configuration are still loaded and their list-shape operations are still queryable, but anything requiring auth will fail at request time with a clear error.
type LoaderOptions ¶
type LoaderOptions struct {
SpecsDir string
}
LoaderOptions controls discovery of spec files. SpecsDir defaults to "./config/specs" when empty. The loader reads every *.yaml/*.yml file in that directory non-recursively.
type OpDescriptor ¶
type OpDescriptor struct {
SpecKey string // YAML filename without extension (e.g. "interaction_studio")
OperationID string // OpenAPI operationId (synthesised when the spec omits it)
Method string // always GET in the read-only first cut
PathTemplate string // OpenAPI path template, e.g. "/users/{userId}"
Mode OpMode
SkipReason string // populated only when Mode == OpModeSkipped
// Parameters bucketed by location — populated for non-skipped operations.
PathParams []ParamSpec
QueryParams []ParamSpec
HeaderParams []ParamSpec
// ResultPath strips the response down to the actual payload (e.g. when
// a list endpoint wraps results in {data: [...]}). Empty means "use
// the response body as-is".
ResultPath []string
// IsArrayResponse signals list-shape responses to the GraphQL adapter
// so it knows whether to expose a singular or plural field.
IsArrayResponse bool
// ExposeAs is the GraphQL field name. Defaulted to
// <spec_key>_<operation_id_snake> by the loader; user config can override.
ExposeAs string
// Join carries the user-supplied parent-table mapping when the
// operation is wired as a row join (Mode == OpModeRowJoin). Nil for
// auto-exposed top-level operations.
Join *JoinConfig
// ResponseSchema points at the success-response body schema. Used
// by the bridge to synthesise DBColumn entries for top-level virtual
// tables so they are visible to GraphQL introspection.
ResponseSchema *openapi3.SchemaRef
// Defaults: fallback values for path/query params; caller args win.
Defaults map[string]string
}
OpDescriptor is the per-operation registry entry produced by the loader. Everything the resolver needs at runtime — URL template, auth, params, result extraction path, exposure config — is denormalised onto this struct so the resolver hot path doesn't re-walk the OpenAPI document.
func (*OpDescriptor) ResolveCallParams ¶ added in v3.18.17
func (op *OpDescriptor) ResolveCallParams(args map[string]string) (CallParams, error)
ResolveCallParams maps args onto CallParams, falling back to op.Defaults. Returns an error if a required path param has neither an arg nor a default.
type OpMode ¶
type OpMode int
OpMode is the GraphJin exposure mode an operation falls into after classification. Each mode corresponds to a distinct integration path: row joins reuse the existing remote_join machinery, top-level modes register virtual tables, and skipped operations are recorded with a reason for boot-time logging.
const ( // OpModeSkipped — the operation cannot be auto-exposed (async, // binary response, mutating verb, etc.). The reason is stored on // OpDescriptor.SkipReason for surfacing in startup logs. OpModeSkipped OpMode = iota // OpModeRowJoin — single-record path operation (e.g. // GET /users/{userId}) that has a JoinConfig in user config and // will be exposed as a child field on the parent DB table. OpModeRowJoin // OpModeSingleByID — single-record path operation without a // JoinConfig. Exposed at the top level with a required argument // matching the path parameter. OpModeSingleByID // OpModeList — collection operation (e.g. GET /audit-logs). // Exposed at the top level as a virtual table whose query-param // filters become GraphQL arguments. OpModeList )
type OperationOverride ¶
type OperationOverride struct {
ExposeAs string `mapstructure:"expose_as" json:"expose_as" yaml:"expose_as"`
ResultPath string `mapstructure:"result_path" json:"result_path" yaml:"result_path"`
Disabled bool `mapstructure:"disabled" json:"disabled" yaml:"disabled"`
ExposeTopLevel bool `mapstructure:"expose_top_level" json:"expose_top_level" yaml:"expose_top_level"`
// Defaults supplies fallback values for path/query params; caller args win.
Defaults map[string]string `mapstructure:"defaults" json:"defaults" yaml:"defaults"`
}
OperationOverride applies presentation tweaks to an auto-classified operation without changing its classification.
type ParamLocation ¶
type ParamLocation string
ParamLocation identifies where a request parameter is carried.
const ( ParamInPath ParamLocation = "path" ParamInQuery ParamLocation = "query" ParamInHeader ParamLocation = "header" )
type ParamSpec ¶
type ParamSpec struct {
Name string
In ParamLocation
Required bool
Type string // OpenAPI primitive type: string | integer | number | boolean
Format string // optional secondary type info (date-time, uuid, etc.)
}
ParamSpec is the loader-resolved view of an OpenAPI parameter, stripped of the spec's full schema apparatus and reduced to what the resolver actually needs at request-build time.
type Registry ¶
type Registry struct {
Specs []*Spec
// contains filtered or unexported fields
}
Registry holds every loaded spec, keyed by spec key, in load order. It is the loader's product and the resolver's input; nothing else in the package mutates it after construction.
func (*Registry) AllOperations ¶
func (r *Registry) AllOperations() []OpDescriptor
AllOperations returns every classified operation across every loaded spec, in (spec, operation) order. Useful for boot logging and tests; the resolver uses Registry.Get to find a spec by key instead.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime is a registry of every loaded SpecRuntime, keyed by spec key. It mirrors Registry's role at the parse layer: the bridge layer in core/ uses Runtime to look up an operation across every spec.
func NewRuntime ¶
NewRuntime builds runtimes for every loaded spec in reg. A failure for a single spec is logged via the warnings slice but does not abort runtime construction — other specs remain available so a misconfigured upstream doesn't take down the rest of GraphJin.
func (*Runtime) AllSpecs ¶
func (r *Runtime) AllSpecs() []*SpecRuntime
AllSpecs returns every loaded spec runtime in deterministic order (whatever order the Registry produced).
func (*Runtime) Caller ¶
Caller resolves an operation across every loaded spec. Returns nil when no spec contains an operation with that id.
type Spec ¶
type Spec struct {
Key string
SourcePath string
Doc *openapi3.T
BaseURL string
Auth AuthConfig
Concurrency ConcurrencyConfig
Operations []OpDescriptor
// SkippedCount is a precomputed tally for boot logging; the per-op
// reasons live on individual OpDescriptors.
SkippedCount int
}
Spec is the loader's per-file output: the parsed OpenAPI document, the resolved per-spec configuration, and the classified operations.
type SpecConfig ¶
type SpecConfig struct {
// BaseURL overrides the spec's servers[0].url. Useful when the spec
// hard-codes a sandbox/prod URL that doesn't match the deployment, or
// when env-var interpolation is needed (e.g. https://${IS_ACCOUNT}...).
BaseURL string `mapstructure:"base_url" json:"base_url" yaml:"base_url"`
// Auth carries credentials and the auth flow shape. One AuthConfig per
// spec — we don't currently support per-operation auth.
Auth AuthConfig `mapstructure:"auth" json:"auth" yaml:"auth"`
// Joins maps an operationId to a DB table/column. Without an entry, an
// operation is still queryable as a top-level field; with an entry, it
// also becomes a child field on the named DB table.
Joins map[string]JoinConfig `mapstructure:"joins" json:"joins" yaml:"joins"`
// Operations carries per-operation overrides that aren't joins (e.g.
// renaming the auto-exposed top-level field, overriding result_path).
// Optional; auto-classification provides sensible defaults.
Operations map[string]OperationOverride `mapstructure:"operations" json:"operations" yaml:"operations"`
// Concurrency bounds the goroutine fan-out and request rate per spec.
// Without this, a 1000-row parent select would spawn 1000 parallel
// HTTP calls to the upstream — a reliable way to get rate-limited.
Concurrency ConcurrencyConfig `mapstructure:"concurrency" json:"concurrency" yaml:"concurrency"`
}
SpecConfig is the per-spec section a user adds to the GraphJin config (keyed by spec filename without extension). Everything that cannot be derived from the OpenAPI document itself lives here: credentials, base URL overrides, DB-to-API join wiring, concurrency caps.
Fields are intentionally permissive at parse time; validation happens in the loader after the spec has been parsed and operations classified, so errors can be reported in terms of operationIds the user recognises.
type SpecRuntime ¶
type SpecRuntime struct {
// contains filtered or unexported fields
}
SpecRuntime is the per-spec runtime built once at boot from a parsed Spec + the resolved AuthConfig. It owns the auth provider, the shared concurrency limiter, and one Caller per active operation.
The bridge layer in core/ uses Runtime.Caller to find the right executor for an inbound GraphQL field; tests use it directly.
func NewSpecRuntime ¶
func NewSpecRuntime(spec *Spec, httpClient *http.Client) (*SpecRuntime, error)
NewSpecRuntime constructs the runtime for one Spec. httpClient is used both by the auth provider (for token endpoints) and by every caller (for operation requests). One client across both is fine and preferred — pooled connections are shared.
func (*SpecRuntime) Caller ¶
func (r *SpecRuntime) Caller(operationID string) (*Caller, bool)
Caller returns the executor for an operationId, or nil + false when the operation either doesn't exist on this spec or was skipped during classification.
func (*SpecRuntime) Spec ¶
func (r *SpecRuntime) Spec() *Spec
Spec returns the underlying parsed spec. Useful for tests and for the bridge layer that needs to enumerate operations during resolver registration.
type TokenExchangeRequest ¶
type TokenExchangeRequest struct {
Method string `mapstructure:"method" json:"method" yaml:"method"` // default POST
BodyFormat string `mapstructure:"body_format" json:"body_format" yaml:"body_format"` // "json" | "form"
Body map[string]interface{} `mapstructure:"body" json:"body" yaml:"body"`
Headers map[string]string `mapstructure:"headers" json:"headers" yaml:"headers"`
}
TokenExchangeRequest describes the POST shape used by token_exchange auth. Body values participate in env-var expansion so credentials never appear in the config file directly.
type TokenExchangeResponse ¶
type TokenExchangeResponse struct {
TokenField string `mapstructure:"token_field" json:"token_field" yaml:"token_field"`
ExpiresField string `mapstructure:"expires_field" json:"expires_field" yaml:"expires_field"` // seconds
}
TokenExchangeResponse describes how to extract the token and TTL from the auth endpoint's JSON response.
type TokenFromRequest ¶
type TokenFromRequest struct {
Header string `mapstructure:"header" json:"header" yaml:"header"`
Query string `mapstructure:"query" json:"query" yaml:"query"`
}
TokenFromRequest forwards credentials carried on the incoming GraphJin request directly to the upstream, enabling multi-tenant patterns where each end-user supplies their own upstream token.