graphql¶
The graphql package provides a type-safe client for Aha.io's GraphQL API, generated by genqlient via gqlforge.
Installation¶
Overview¶
The GraphQL API provides access to functionality not available in the REST API, including:
- Full-text search across documents
- Creating features and ideas with validation
- Setting custom field values on any record
- Discovering required fields and custom field definitions
Quick Start¶
package main
import (
"context"
"fmt"
"log"
"github.com/grokify/aha-go/graphql"
"github.com/grokify/aha-go/graphql/generated"
)
func main() {
// Create a genqlient-compatible client
client := graphql.NewGenqlientClient("mycompany", "your-api-key")
ctx := context.Background()
// Get a feature with full type safety
resp, err := generated.GetFeature(ctx, client, "FEAT-123")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Feature: %s - %s\n", resp.Feature.ReferenceNum, resp.Feature.Name)
}
Client¶
Creating a Client¶
// Basic client (recommended)
client := graphql.NewGenqlientClient("subdomain", "api-key")
// With custom HTTP client
httpClient := &http.Client{Timeout: 30 * time.Second}
client := graphql.NewGenqlientClientWithHTTP("subdomain", "api-key", httpClient)
Queries¶
All queries are type-safe and return strongly-typed responses.
GetFeature¶
resp, err := generated.GetFeature(ctx, client, "FEAT-123")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Name: %s\n", resp.Feature.Name)
fmt.Printf("Status: %s\n", resp.Feature.WorkflowStatus.Name)
SearchDocuments¶
Search across pages, features, and other document types.
resp, err := generated.SearchDocuments(ctx, client, "onboarding", []string{"Page", "Feature"})
if err != nil {
log.Fatal(err)
}
for _, node := range resp.SearchDocuments.Nodes {
fmt.Printf("Name: %s\n", *node.Name)
fmt.Printf("URL: %s\n", node.Url)
fmt.Printf("Type: %s\n", node.SearchableType)
}
Available Queries¶
| Query | Description |
|---|---|
GetFeature |
Get feature by reference number |
GetPage |
Get page by reference number |
GetIdea |
Get idea by reference number |
GetRelease |
Get release by reference number |
GetGoal |
Get goal by reference number |
GetEpic |
Get epic by reference number |
GetInitiative |
Get initiative by reference number |
GetRequirement |
Get requirement by reference number |
GetProject |
Get project by ID |
GetAccount |
Get account information |
SearchDocuments |
Full-text search across documents |
GetFeatureWithIntegrations |
Get feature with integration fields |
GetFeatureWithLinks |
Get feature with record links |
GetFeatureScreenDefinition |
Get required fields configuration |
GetProjectCustomFields |
Get custom field definitions |
GetFeatureWithCustomFields |
Get feature with custom field values |
Mutations¶
Creating Features¶
// Create a feature in a release
resp, err := generated.CreateFeatureWithRelease(ctx, client,
"My New Feature", // name
"RELEASE-ID", // releaseId
"<p>Description here</p>", // description
"tag1, tag2", // tagList
nil, // skipRequiredFieldsValidation
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created: %s\n", resp.CreateFeature.Feature.ReferenceNum)
// Create a feature in a project (without specifying release)
resp, err := generated.CreateFeatureWithProject(ctx, client,
"Feature Name",
"PROJECT-ID",
"<p>Description</p>",
"",
nil,
)
// Create a feature with full assignment options
resp, err := generated.CreateFeatureWithAssignments(ctx, client,
"Feature Name", // name
"RELEASE-ID", // releaseId
"<p>Description</p>", // description
"tags", // tagList
nil, // skipRequiredFieldsValidation
"INITIATIVE-ID", // initiativeId
"EPIC-ID", // epicId
"USER-ID", // assignedToUserId
)
Creating Ideas¶
resp, err := generated.CreateIdea(ctx, client,
"New Idea", // name
"PROJECT-ID", // projectId
nil, // skipRequiredFieldsValidation
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created: %s\n", resp.CreateIdea.Idea.ReferenceNum)
Setting Custom Fields¶
Set custom field values on any record type:
resp, err := generated.SetCustomFieldValues(ctx, client,
"FEATURE-ID",
generated.CustomFieldableTypeEnumFeature,
[]generated.CustomFieldValueInput{
{Key: "priority_score", Value: 85},
{Key: "customer_segment", Value: "Enterprise"},
{Key: "target_date", Value: "2024-06-15"},
},
)
if err != nil {
log.Fatal(err)
}
Supported record types for custom fields:
CustomFieldableTypeEnumFeatureCustomFieldableTypeEnumIdeaCustomFieldableTypeEnumEpicCustomFieldableTypeEnumInitiativeCustomFieldableTypeEnumGoalCustomFieldableTypeEnumRelease
Available Mutations¶
| Mutation | Description |
|---|---|
CreateFeatureWithRelease |
Create feature in a release |
CreateFeatureWithProject |
Create feature in a project |
CreateFeatureWithAssignments |
Create feature with full assignments |
CreateIdea |
Create an idea |
SetCustomFieldValues |
Set custom fields on any record |
UpdateFeatureName |
Update feature name |
UpdateFeatureDescription |
Update feature description |
UpdateFeatureStatus |
Update feature workflow status |
UpdateFeatureTags |
Update feature tags |
AssignFeatureToInitiative |
Assign feature to initiative |
AssignFeatureToRelease |
Assign feature to release |
AssignFeatureToEpic |
Assign feature to epic |
AssignFeatureToUser |
Assign feature to user |
PromoteIdeaToFeature |
Promote idea to feature |
PromoteIdeaToEpic |
Promote idea to epic |
CreateRecordLink |
Create link between records |
Required Field Validation¶
Helper functions to discover and validate required fields before creating records.
Discover Required Fields¶
import "github.com/grokify/aha-go/graphql"
// Get required fields for features (pass any existing feature ID)
reqs, err := graphql.GetFeatureRequirements(ctx, client, "EXISTING-FEATURE-ID")
if err != nil {
log.Fatal(err)
}
// Get list of required field IDs
requiredIDs := reqs.RequiredFieldIDs()
fmt.Printf("Required fields: %v\n", requiredIDs)
// Get required custom field keys
customKeys := reqs.RequiredCustomFieldKeys()
fmt.Printf("Required custom fields: %v\n", customKeys)
// Check if a specific field is required
if reqs.IsRequired("description") {
fmt.Println("Description is required")
}
Validate Before Creating¶
// Validate that all required fields are provided
missing := graphql.ValidateRequiredFields(reqs, map[string]any{
"name": "Feature Name",
"description": "Description",
})
if len(missing) > 0 {
fmt.Printf("Missing required fields: %v\n", missing)
return
}
// Now safe to create
resp, err := generated.CreateFeatureWithRelease(ctx, client, ...)
Get Custom Field Definitions¶
Discover available custom fields for a project:
defs, err := graphql.GetProjectCustomFieldDefinitions(ctx, client, "PROJECT-ID")
if err != nil {
log.Fatal(err)
}
for _, def := range defs {
fmt.Printf("Field: %s (key: %s, type: %s)\n", def.Name, def.Key, def.Type)
if len(def.Options) > 0 {
fmt.Println(" Options:")
for _, opt := range def.Options {
fmt.Printf(" - %s\n", opt.Name)
}
}
}
Error Handling¶
resp, err := generated.GetFeature(ctx, client, "INVALID")
if err != nil {
// Check error type
fmt.Printf("Error: %v\n", err)
return
}
if resp.Feature == nil {
fmt.Println("Feature not found")
return
}
Example Client (Learning Reference)¶
The graphql/example package provides a handwritten GraphQL client as educational reference code for users learning to write GraphQL clients without code generation.
import "github.com/grokify/aha-go/graphql/example"
client := example.NewClient("mycompany", "your-api-key")
var result struct {
Feature struct {
Name string `json:"name"`
Description struct {
MarkdownBody string `json:"markdownBody"`
} `json:"description"`
} `json:"feature"`
}
err := client.Query(ctx, example.GetFeatureQuery, map[string]any{"id": "FEAT-123"}, &result)
For production use, the generated client is recommended.
Regenerating the Client¶
If the Aha.io schema changes, regenerate the client:
# Install gqlforge
go install github.com/grokify/gqlforge/cmd/gqlforge@latest
# Validate operations against schema
gqlforge validate --schema graphql/schema.graphql --operations "graphql/operations/*.graphql"
# Regenerate typed client
gqlforge generate
API Reference¶
For complete API documentation, see pkg.go.dev.