fix(api): surface GraphQL playground metadata

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 04:38:22 +00:00
parent 1491e16f9e
commit 76acb4534b
8 changed files with 67 additions and 19 deletions

View file

@ -31,9 +31,10 @@ type graphqlConfig struct {
//
// cfg := api.GraphQLConfig{Enabled: true, Path: "/graphql", Playground: true}
type GraphQLConfig struct {
Enabled bool
Path string
Playground bool
Enabled bool
Path string
Playground bool
PlaygroundPath string
}
// GraphQLConfig returns the currently configured GraphQL settings for the engine.
@ -56,6 +57,9 @@ func (e *Engine) GraphQLConfig() GraphQLConfig {
if e.graphql != nil {
cfg.Path = normaliseGraphQLPath(e.graphql.path)
if e.graphql.playground {
cfg.PlaygroundPath = cfg.Path + "/playground"
}
}
return cfg

View file

@ -30,13 +30,16 @@ func TestEngine_GraphQLConfig_Good_SnapshotsCurrentSettings(t *testing.T) {
if !cfg.Playground {
t.Fatal("expected GraphQL playground to be enabled")
}
if cfg.PlaygroundPath != "/gql/playground" {
t.Fatalf("expected GraphQL playground path /gql/playground, got %q", cfg.PlaygroundPath)
}
}
func TestEngine_GraphQLConfig_Good_EmptyOnNilEngine(t *testing.T) {
var e *api.Engine
cfg := e.GraphQLConfig()
if cfg.Enabled || cfg.Path != "" || cfg.Playground {
if cfg.Enabled || cfg.Path != "" || cfg.Playground || cfg.PlaygroundPath != "" {
t.Fatalf("expected zero-value GraphQL config, got %+v", cfg)
}
}

View file

@ -150,6 +150,9 @@ func TestEngine_RuntimeConfig_Good_SnapshotsCurrentSettings(t *testing.T) {
if cfg.Transport.SwaggerPath != "/docs" {
t.Fatalf("expected transport swagger path /docs, got %q", cfg.Transport.SwaggerPath)
}
if cfg.Transport.GraphQLPlaygroundPath != "/graphql/playground" {
t.Fatalf("expected transport graphql playground path /graphql/playground, got %q", cfg.Transport.GraphQLPlaygroundPath)
}
if !cfg.Cache.Enabled || cfg.Cache.TTL != 5*time.Minute {
t.Fatalf("expected cache snapshot to be populated, got %+v", cfg.Cache)
}
@ -162,6 +165,9 @@ func TestEngine_RuntimeConfig_Good_SnapshotsCurrentSettings(t *testing.T) {
if !cfg.GraphQL.Playground {
t.Fatal("expected GraphQL playground snapshot to be enabled")
}
if cfg.GraphQL.PlaygroundPath != "/graphql/playground" {
t.Fatalf("expected GraphQL playground path /graphql/playground, got %q", cfg.GraphQL.PlaygroundPath)
}
if cfg.I18n.DefaultLocale != "en-GB" {
t.Fatalf("expected default locale en-GB, got %q", cfg.I18n.DefaultLocale)
}

View file

@ -34,6 +34,7 @@ type SpecBuilder struct {
GraphQLEnabled bool
GraphQLPath string
GraphQLPlayground bool
GraphQLPlaygroundPath string
WSPath string
WSEnabled bool
SSEPath string
@ -124,6 +125,9 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
spec["x-graphql-path"] = normaliseOpenAPIPath(graphqlPath)
spec["x-graphql-playground"] = sb.GraphQLPlayground
}
if playgroundPath := sb.effectiveGraphQLPlaygroundPath(); playgroundPath != "" {
spec["x-graphql-playground-path"] = normaliseOpenAPIPath(playgroundPath)
}
if wsPath := sb.effectiveWSPath(); wsPath != "" {
spec["x-ws-path"] = normaliseOpenAPIPath(wsPath)
}
@ -1907,6 +1911,26 @@ func (sb *SpecBuilder) effectiveGraphQLPath() string {
return graphqlPath
}
// effectiveGraphQLPlaygroundPath returns the configured playground path when
// GraphQL playground is enabled.
func (sb *SpecBuilder) effectiveGraphQLPlaygroundPath() string {
if !sb.GraphQLPlayground {
return ""
}
path := strings.TrimSpace(sb.GraphQLPlaygroundPath)
if path != "" {
return path
}
base := sb.effectiveGraphQLPath()
if base == "" {
base = defaultGraphQLPath
}
return base + "/playground"
}
// effectiveSwaggerPath returns the configured Swagger UI path or the default
// path when Swagger is enabled without an explicit override.
func (sb *SpecBuilder) effectiveSwaggerPath() string {
@ -1979,6 +2003,7 @@ func (sb *SpecBuilder) snapshot() *SpecBuilder {
out.Version = strings.TrimSpace(out.Version)
out.SwaggerPath = strings.TrimSpace(out.SwaggerPath)
out.GraphQLPath = strings.TrimSpace(out.GraphQLPath)
out.GraphQLPlaygroundPath = strings.TrimSpace(out.GraphQLPlaygroundPath)
out.WSPath = strings.TrimSpace(out.WSPath)
out.SSEPath = strings.TrimSpace(out.SSEPath)
out.TermsOfService = strings.TrimSpace(out.TermsOfService)

View file

@ -611,10 +611,11 @@ func TestSpecBuilder_Good_GraphQLEndpoint(t *testing.T) {
func TestSpecBuilder_Good_GraphQLPlaygroundEndpoint(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",
Version: "1.0.0",
GraphQLPath: "/graphql",
GraphQLPlayground: true,
Title: "Test",
Version: "1.0.0",
GraphQLPath: "/graphql",
GraphQLPlayground: true,
GraphQLPlaygroundPath: "/graphql/playground",
}
data, err := sb.Build(nil)
@ -637,6 +638,9 @@ func TestSpecBuilder_Good_GraphQLPlaygroundEndpoint(t *testing.T) {
if getOp["operationId"] != "get_graphql_playground" {
t.Fatalf("expected playground operationId to be get_graphql_playground, got %v", getOp["operationId"])
}
if got := spec["x-graphql-playground-path"]; got != "/graphql/playground" {
t.Fatalf("expected x-graphql-playground-path=/graphql/playground, got %v", got)
}
responses := getOp["responses"].(map[string]any)
success := responses["200"].(map[string]any)

View file

@ -69,6 +69,7 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder {
builder.GraphQLEnabled = runtime.GraphQL.Enabled
builder.GraphQLPath = runtime.GraphQL.Path
builder.GraphQLPlayground = runtime.GraphQL.Playground
builder.GraphQLPlaygroundPath = runtime.GraphQL.PlaygroundPath
builder.WSPath = runtime.Transport.WSPath
builder.WSEnabled = runtime.Transport.WSEnabled
builder.SSEPath = runtime.Transport.SSEPath

View file

@ -100,6 +100,9 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
if got := spec["x-graphql-playground"]; got != true {
t.Fatalf("expected x-graphql-playground=true, got %v", got)
}
if got := spec["x-graphql-playground-path"]; got != "/gql/playground" {
t.Fatalf("expected x-graphql-playground-path=/gql/playground, got %v", got)
}
if got := spec["x-ws-path"]; got != "/socket" {
t.Fatalf("expected x-ws-path=/socket, got %v", got)
}

View file

@ -13,17 +13,18 @@ import "strings"
//
// cfg := api.TransportConfig{SwaggerPath: "/swagger", WSPath: "/ws"}
type TransportConfig struct {
SwaggerEnabled bool
SwaggerPath string
GraphQLPath string
GraphQLEnabled bool
GraphQLPlayground bool
WSEnabled bool
WSPath string
SSEEnabled bool
SSEPath string
PprofEnabled bool
ExpvarEnabled bool
SwaggerEnabled bool
SwaggerPath string
GraphQLPath string
GraphQLEnabled bool
GraphQLPlayground bool
GraphQLPlaygroundPath string
WSEnabled bool
WSPath string
SSEEnabled bool
SSEPath string
PprofEnabled bool
ExpvarEnabled bool
}
// TransportConfig returns the currently configured transport metadata for the engine.
@ -49,6 +50,7 @@ func (e *Engine) TransportConfig() TransportConfig {
gql := e.GraphQLConfig()
cfg.GraphQLEnabled = gql.Enabled
cfg.GraphQLPlayground = gql.Playground
cfg.GraphQLPlaygroundPath = gql.PlaygroundPath
if e.swaggerEnabled || strings.TrimSpace(e.swaggerPath) != "" {
cfg.SwaggerPath = resolveSwaggerPath(e.swaggerPath)