fix(api): support custom GraphQL playground paths

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 04:53:30 +00:00
parent 3896896090
commit a3a1c20e7a
4 changed files with 55 additions and 1 deletions

View file

@ -77,6 +77,7 @@ func registerSpecBuilderFlags(cmd *cli.Command, cfg *specBuilderConfig) {
cli.StringFlag(cmd, &cfg.swaggerPath, "swagger-path", "", "", "Swagger UI path in generated spec")
cli.StringFlag(cmd, &cfg.graphqlPath, "graphql-path", "", "", "GraphQL endpoint path in generated spec")
cli.BoolFlag(cmd, &cfg.graphqlPlayground, "graphql-playground", "", false, "Include the GraphQL playground endpoint in generated spec")
cli.StringFlag(cmd, &cfg.graphqlPlaygroundPath, "graphql-playground-path", "", "", "GraphQL playground path in generated spec")
cli.StringFlag(cmd, &cfg.ssePath, "sse-path", "", "", "SSE endpoint path in generated spec")
cli.StringFlag(cmd, &cfg.wsPath, "ws-path", "", "", "WebSocket endpoint path in generated spec")
cli.BoolFlag(cmd, &cfg.pprofEnabled, "pprof", "", false, "Include pprof endpoints in generated spec")

View file

@ -103,6 +103,9 @@ func TestAPISpecCmd_Good_JSON(t *testing.T) {
if specCmd.Flag("graphql-playground") == nil {
t.Fatal("expected --graphql-playground flag on spec command")
}
if specCmd.Flag("graphql-playground-path") == nil {
t.Fatal("expected --graphql-playground-path flag on spec command")
}
if specCmd.Flag("sse-path") == nil {
t.Fatal("expected --sse-path flag on spec command")
}
@ -456,6 +459,50 @@ func TestAPISpecCmd_Good_GraphQLPlaygroundFlagPopulatesSpecPaths(t *testing.T) {
}
}
func TestAPISpecCmd_Good_GraphQLPlaygroundPathFlagOverridesGeneratedPath(t *testing.T) {
root := &cli.Command{Use: "root"}
AddAPICommands(root)
outputFile := t.TempDir() + "/spec.json"
root.SetArgs([]string{
"api", "spec",
"--graphql-path", "/graphql",
"--graphql-playground",
"--graphql-playground-path", "/graphql-ui",
"--output", outputFile,
})
root.SetErr(new(bytes.Buffer))
if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile(outputFile)
if err != nil {
t.Fatalf("expected spec file to be written: %v", err)
}
var spec map[string]any
if err := json.Unmarshal(data, &spec); err != nil {
t.Fatalf("expected valid JSON spec, got error: %v", err)
}
paths, ok := spec["paths"].(map[string]any)
if !ok {
t.Fatal("expected paths object in generated spec")
}
if _, ok := paths["/graphql-ui"]; !ok {
t.Fatal("expected custom GraphQL playground path in generated spec")
}
if _, ok := paths["/graphql/playground"]; ok {
t.Fatal("expected default GraphQL playground path to be overridden")
}
if got := spec["x-graphql-playground-path"]; got != "/graphql-ui" {
t.Fatalf("expected x-graphql-playground-path=/graphql-ui, got %v", got)
}
}
func TestAPISpecCmd_Good_EnabledExtensionsFollowProvidedPaths(t *testing.T) {
root := &cli.Command{Use: "root"}
AddAPICommands(root)

View file

@ -17,6 +17,7 @@ type specBuilderConfig struct {
swaggerPath string
graphqlPath string
graphqlPlayground bool
graphqlPlaygroundPath string
ssePath string
wsPath string
pprofEnabled bool
@ -61,6 +62,7 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) {
GraphQLEnabled: graphqlPath != "" || cfg.graphqlPlayground,
GraphQLPath: graphqlPath,
GraphQLPlayground: cfg.graphqlPlayground,
GraphQLPlaygroundPath: strings.TrimSpace(cfg.graphqlPlaygroundPath),
SSEEnabled: ssePath != "",
SSEPath: ssePath,
WSEnabled: wsPath != "",

View file

@ -299,7 +299,11 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
}
paths[graphqlPath] = item
if sb.GraphQLPlayground {
playgroundPath := normaliseOpenAPIPath(graphqlPath + "/playground")
playgroundPath := sb.effectiveGraphQLPlaygroundPath()
if playgroundPath == "" {
playgroundPath = graphqlPath + "/playground"
}
playgroundPath = normaliseOpenAPIPath(playgroundPath)
item := graphqlPlaygroundPathItem(playgroundPath, operationIDs)
if isPublicPathForList(playgroundPath, publicPaths) {
makePathItemPublic(item)