From 76acb4534b9fffb7cb7e4df81a7f91503e44fc12 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 04:38:22 +0000 Subject: [PATCH] fix(api): surface GraphQL playground metadata Co-Authored-By: Virgil --- graphql.go | 10 +++++++--- graphql_config_test.go | 5 ++++- modernization_test.go | 6 ++++++ openapi.go | 25 +++++++++++++++++++++++++ openapi_test.go | 12 ++++++++---- spec_builder_helper.go | 1 + spec_builder_helper_test.go | 3 +++ transport.go | 24 +++++++++++++----------- 8 files changed, 67 insertions(+), 19 deletions(-) diff --git a/graphql.go b/graphql.go index 4825b6a..0a3298c 100644 --- a/graphql.go +++ b/graphql.go @@ -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 diff --git a/graphql_config_test.go b/graphql_config_test.go index 4ef1617..83bbc8d 100644 --- a/graphql_config_test.go +++ b/graphql_config_test.go @@ -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) } } diff --git a/modernization_test.go b/modernization_test.go index fc2ed46..b6e3a7d 100644 --- a/modernization_test.go +++ b/modernization_test.go @@ -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) } diff --git a/openapi.go b/openapi.go index 93434e3..d032aaa 100644 --- a/openapi.go +++ b/openapi.go @@ -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) diff --git a/openapi_test.go b/openapi_test.go index c2207ed..b983392 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -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) diff --git a/spec_builder_helper.go b/spec_builder_helper.go index 1c8f1e4..5eaa8e6 100644 --- a/spec_builder_helper.go +++ b/spec_builder_helper.go @@ -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 diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index 3754d25..8654ed8 100644 --- a/spec_builder_helper_test.go +++ b/spec_builder_helper_test.go @@ -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) } diff --git a/transport.go b/transport.go index 59c7517..a090729 100644 --- a/transport.go +++ b/transport.go @@ -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)