openapi

package
v3.18.17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

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
	OnUnauthorized(ctx context.Context) 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.

func (*Caller) Call

func (c *Caller) Call(ctx context.Context, p CallParams) ([]byte, error)

Call executes the operation and returns the response body, with the configured ResultPath stripped. A 401 triggers exactly one retry after invalidating the auth provider's cached token — this handles the common case of a service-credential token expiring mid-flight.

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

type LoadResult struct {
	Registry *Registry
	Warnings []string
}

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
)

func (OpMode) String

func (m OpMode) String() string

String returns a human-readable mode label for log output.

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.

func (*Registry) Get

func (r *Registry) Get(key string) (*Spec, bool)

Get looks up a spec by key. The second return is false when no spec with that key has been loaded.

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

func NewRuntime(reg *Registry, httpClient *http.Client) (*Runtime, []string, error)

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

func (r *Runtime) Caller(specKey, operationID string) (*Caller, bool)

Caller resolves an operation across every loaded spec. Returns nil when no spec contains an operation with that id.

func (*Runtime) Operation

func (r *Runtime) Operation(specKey, operationID string) (*OpDescriptor, bool)

Operation returns the OpDescriptor for a (spec, operation) pair.

func (*Runtime) Spec

func (r *Runtime) Spec(specKey string) (*SpecRuntime, bool)

Spec returns the runtime for a spec key.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL