Skip to content

API Implementation

Technical details of the GraphQL API integration in the pfy CLI.

Architecture

The CLI implements a type-safe GraphQL client using struct-based queries and mutations.

Components

cli/
├── pfy/
│   └── main.go                 # Entry point
└── internal/
    ├── api/
    │   ├── client.go           # GraphQL client with auth
    │   ├── operations.go       # Type-safe operations
    │   └── config.go           # Configuration management
    └── app/
        ├── app.go              # CLI app setup
        ├── commands.go         # Command definitions
        └── helpers.go          # Utility functions

GraphQL Client

Implementation

Built on hasura/go-graphql-client:

go
type Client struct {
    gqlClient  *graphql.Client
    httpClient *http.Client
    endpoint   string
    token      string
}

The client maintains both a GraphQL client for API operations and an HTTP client for REST endpoints (e.g., pilet uploads).

Authentication

Bearer token authentication via WithRequestModifier:

go
gqlClient = gqlClient.WithRequestModifier(func(r *http.Request) {
    r.Header.Set("Authorization", "Bearer "+token)
})

Type-Safe Operations

Struct-Based Queries

GraphQL operations are defined as Go structs with graphql tags:

go
func (c *Client) GetProjects(ctx context.Context, filters []Filter, order Order, pagination Pagination) ([]Project, error) {
    var query struct {
        Projects []Project `graphql:"projects(filters: $filters, order: $order, pagination: $pagination)"`
    }
    // ... execute query with variables
    return query.Projects, nil
}

Domain Types

The CLI defines domain types that map to GraphQL types:

go
type Project struct {
    ID          graphql.ID     `graphql:"id" json:"id"`
    Slug        graphql.String `graphql:"slug" json:"slug"`
    Name        graphql.String `graphql:"name" json:"name"`
    Description graphql.String `graphql:"description" json:"description"`
}

type Application struct {
    ID          graphql.ID     `graphql:"id" json:"id"`
    Slug        graphql.String `graphql:"slug" json:"slug"`
    Name        graphql.String `graphql:"name" json:"name"`
    Description graphql.String `graphql:"description" json:"description"`
    Version     graphql.String `graphql:"version" json:"version"`
}
// ... similar for Tenant, LanguagePack

GraphQL Types

Type mapping from Go to GraphQL:

Go TypeGraphQL TypeUsage
graphql.StringStringText fields
graphql.IDIDUnique identifiers
graphql.IntIntInteger numbers
graphql.BooleanBooleanTrue/false values
graphql.FloatFloatDecimal numbers

All domain types use graphql tags for field mapping and json tags for JSON serialization.

Mutations

Mutations use input types:

go
type CreateProjectInput struct {
    Slug        graphql.String `json:"slug"`
    Name        graphql.String `json:"name"`
    Description graphql.String `json:"description,omitempty"`
}

func (c *Client) CreateProject(ctx context.Context, input CreateProjectInput) (*Project, error) {
    var mutation struct {
        CreateProject Project `graphql:"createProject(input: $input)"`
    }
    // ... execute mutation
    return &mutation.CreateProject, nil
}

Application Promotion and Copying

Special mutations for application deployment workflows:

go
type PromoteApplicationInput struct {
    ApplicationID  graphql.ID     `json:"applicationId"`
    TargetTenantID graphql.ID     `json:"targetTenantId"`
    Slug           graphql.String `json:"slug"`
    Name           graphql.String `json:"name,omitempty"`
    Version        graphql.String `json:"version,omitempty"`
}

type CopyApplicationInput struct {
    SourceApplicationID graphql.ID     `json:"sourceApplicationId"`
    TargetTenantID      graphql.ID     `json:"targetTenantId"`
    Slug                graphql.String `json:"slug"`
    // ... other fields
}

Implemented Operations

Projects

Queries:

  • GetProjects - List projects with filters, order, pagination
  • GetProject - Get project by slug

Mutations:

  • CreateProject - Create new project
  • UpdateProject - Update existing project (not exposed in CLI)
  • DeleteProject - Delete project (not exposed in CLI)

Tenants

Queries:

  • GetTenants - List tenants in project
  • GetTenant - Get tenant by project ID and slug

Mutations:

  • CreateTenant - Create tenant in project
  • UpdateTenant - Update tenant (not exposed in CLI)
  • DeleteTenant - Delete tenant (not exposed in CLI)

Applications

Queries:

  • GetApplications - List applications in tenant
  • GetApplication - Get application by tenant ID and slug

Mutations:

  • CreateApplication - Create application in tenant
  • UpdateApplication - Update application (not exposed in CLI)
  • DeleteApplication - Delete application (not exposed in CLI)

Language Packs

Queries:

  • GetLanguagePacks - List language packs in project

Mutations:

  • CreateLanguagePack - Create language pack (planned)
  • DeleteLanguagePack - Delete language pack (planned)

Filters, Ordering, and Pagination

Filter, Order, and Pagination

go
type Filter struct {
    Field    graphql.String `json:"field"`
    Operator graphql.String `json:"operator"`
    Value    interface{}    `json:"value"`
}

type Order struct {
    Field     graphql.String `json:"field"`
    Direction graphql.String `json:"direction"` // "asc" or "desc"
}

type Pagination struct {
    Offset graphql.Int `json:"offset"`
    Limit  graphql.Int `json:"limit"`
}

Configuration Management

Config Structure

go
type Config struct {
    ManagerURL string `yaml:"manager_url"`
    Token      string `yaml:"token"`
}

Configuration is loaded from ~/.pfy/config.yaml and can be overridden by environment variables (PFY_MANAGER_URL, PFY_MANAGER_TOKEN).

Configuration Priority

  1. Command-line flags (highest priority)
  2. Environment variables
  3. Config file (lowest priority)

Error Handling

All operations wrap errors with context:

go
if err := c.Query(ctx, &query, variables); err != nil {
    return nil, fmt.Errorf("failed to get projects: %w", err)
}

GraphQL errors include field paths and detailed messages for debugging.

REST Endpoints

Pilet Upload

The client includes an UploadPilet method for uploading pilet packages via multipart/form-data:

go
func (c *Client) UploadPilet(ctx context.Context, projectID, name, version, description, filePath string) error {
    // Creates multipart form with file upload
    // POST /pilets/{projectID}/{name}/{version}
}

Best Practices

  1. Use struct tags - Define GraphQL fields with graphql tags
  2. Type safety - Leverage Go's type system with GraphQL types
  3. Error wrapping - Provide context with error messages
  4. Configuration hierarchy - Support flags, env vars, and config files
  5. Secure by default - Use HTTPS and verify certificates
  6. Token security - Store tokens with restricted permissions (0600)
  7. Verbose mode - Provide detailed output for debugging
  8. Quiet mode - Enable scripting with minimal output

Future Enhancements

  • [ ] Batch operations for multiple resources
  • [ ] Advanced filtering and search
  • [ ] Export/import configurations
  • [ ] Offline mode with caching
  • [ ] GraphQL subscription support
  • [ ] Custom output formats (JSON, YAML, table)
  • [ ] Shell completion for resource names
  • [ ] Interactive mode for complex operations