Appearance
Using the API from Go
This example uses only the Go standard library (net/http and encoding/json), so there is nothing to go get. It needs an API token and the COCOCO_URL and COCOCO_TOKEN environment variables described in Calling the CoCoCo API.
The operations
graphql
query ListJobs($first: Int, $after: String) {
listJobs(first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id name quantity status dueAt } }
}
}graphql
mutation CreateJob($input: CreateJobInput!) {
createJob(input: $input) {
job { id name status }
errors { path message }
}
}The program
Save this as main.go in a new module (go mod init example.com/jobs). go run . lists every job; go run . create creates one.
go
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
const listJobsQuery = `query ListJobs($first: Int, $after: String) {
listJobs(first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id name quantity status dueAt } }
}
}`
const createJobMutation = `mutation CreateJob($input: CreateJobInput!) {
createJob(input: $input) {
job { id name status }
errors { path message }
}
}`
type Job struct {
ID string `json:"id"` // a JobID such as "job_01M0SYJHB7FX6BKYVZZ7EAG4Y8"
Name string `json:"name"`
Quantity int `json:"quantity"`
Status string `json:"status"`
DueAt *string `json:"dueAt"` // ISO 8601, or nil
}
type FieldError struct {
Path string `json:"path"`
Message string `json:"message"`
}
type listJobsData struct {
ListJobs struct {
TotalCount *int `json:"totalCount"`
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
EndCursor *string `json:"endCursor"`
} `json:"pageInfo"`
Edges []struct {
Node Job `json:"node"`
} `json:"edges"`
} `json:"listJobs"`
}
type createJobData struct {
CreateJob struct {
Job *Job `json:"job"`
Errors []FieldError `json:"errors"`
} `json:"createJob"`
}
type Client struct {
Endpoint string
Token string
HTTP *http.Client
}
// Do posts one operation and decodes its data into out.
// It returns an error for an HTTP error status and for any top-level GraphQL error.
func (c *Client) Do(query string, variables map[string]any, out any) error {
body, err := json.Marshal(map[string]any{"query": query, "variables": variables})
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, c.Endpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, msg)
}
var result struct {
Data json.RawMessage `json:"data"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
// Authentication, permission and query errors arrive here with HTTP 200.
if len(result.Errors) > 0 {
msgs := make([]string, len(result.Errors))
for i, e := range result.Errors {
msgs[i] = e.Message
}
return errors.New(strings.Join(msgs, "; "))
}
return json.Unmarshal(result.Data, out)
}
// ListJobs returns every job, fetching 100 per request.
func (c *Client) ListJobs() ([]Job, error) {
var jobs []Job
var after *string
for {
var data listJobsData
if err := c.Do(listJobsQuery, map[string]any{"first": 100, "after": after}, &data); err != nil {
return nil, err
}
for _, edge := range data.ListJobs.Edges {
jobs = append(jobs, edge.Node)
}
if !data.ListJobs.PageInfo.HasNextPage {
return jobs, nil
}
after = data.ListJobs.PageInfo.EndCursor
}
}
// CreateJob creates a job. Validation problems come back in the payload.
func (c *Client) CreateJob(name string, quantity int, externalID string) (*Job, error) {
input := map[string]any{"name": name, "quantity": quantity, "externalId": externalID}
var data createJobData
if err := c.Do(createJobMutation, map[string]any{"input": input}, &data); err != nil {
return nil, err
}
if len(data.CreateJob.Errors) > 0 {
e := data.CreateJob.Errors[0]
return nil, fmt.Errorf("%s: %s", e.Path, e.Message)
}
return data.CreateJob.Job, nil
}
func main() {
client := &Client{
Endpoint: strings.TrimRight(os.Getenv("COCOCO_URL"), "/") + "/graphql",
Token: os.Getenv("COCOCO_TOKEN"),
HTTP: &http.Client{Timeout: 30 * time.Second},
}
if len(os.Args) > 1 && os.Args[1] == "create" {
job, err := client.CreateJob("Business cards", 500, "ERP-10042")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created %s (%s)\n", job.ID, job.Status)
return
}
jobs, err := client.ListJobs()
if err != nil {
log.Fatal(err)
}
for _, job := range jobs {
fmt.Println(job.ID, job.Status, job.Quantity, job.Name)
}
}Notes
Doreturns an error for both kinds of failure: an HTTP error status, and a response whoseerrorsarray is not empty.ListJobsfollowspageInfo.endCursoruntilhasNextPageisfalse. The API returns at most 100 items per page.externalIdmust be unique in your tenant. Runningcreatetwice with the same value returns a validation error increateJob.errors.- The code needs Go 1.18 or later (for
any).