Inhalt

Daten vom GraphQL Endpunkt von Shopify holen

Ich habe gestern für einen unserer Backend-Services einfach nur einen Preis aus unserem Shopify Shop gebraucht. Um genau zu sein brauchte ich eine Möglichkeit um bei Shopify anhand einer Artikelnummer/SKU einen Preis abzufragen.

Da es sich hier um eine sehr gezielte Anfrage handelt, habe ich mir gedacht, dass ich hierzu die GraphQL Api von Shopify nutze 😊 Im folgenden zeige ich einmal wie man ein derartiges Problem lösen kann. Der vollständige Code ist unter folgender URL zu finden:

https://gist.github.com/m3tam3re/98d9b57f404db3bc1ff8af133e54afd0

Was brauchen wir?

Zunächst einmal muss unser Programm ein Http Request an den GraphQL Endpunkt von Shopify senden. Mit diesem Request wollen wir natürlich eine konkrete Anfrage in Form einer Query senden. D.h. wir brauchen eine Funktion, die unsere Query anhand einer gegebenen Artikelnummer erzeugt und das Ergebnis verarbeitet und uns den Preis zurückgibt.

Um das umzusetzen brauchen wir eigentlich keinerlei zusätzliche Pakete. Das alles lässt sich mit dem Standardumfang von GO lösen. Da ich allerdings für mich den Umgang mit der JSON Antwort vereinfachen wollte habe ich mich entschieden das Paket github.com/valyala/fastjson zu verwenden. Wenn Ihr Euch den Code anseht versteht Ihr schnell wieso 😇

1. Funktion für HTTP Request erstellen

Als erstes erstellen wir eine Funktion, die ein Request an den GraphQL Endpunkt unseres Shopify Shops sendet und uns die Antwort oder einen Fehler zurückliefert. Da ich davon ausgehe, dass ich vielleicht auch noch andere Daten brauche außer dem aktuellen Anwendungsfall habe ich mich entschieden das Ganze in eine allgemeine Funktion zu packen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func startRequest(body []byte) (*http.Response, error) {
	client := http.Client{
		Timeout: time.Second * 120,
	}
	req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf( "error building request: %s", err)
	}
	req.Header.Add("X-Shopify-Access-Token",token)
	req.Header.Add("Content-Type", "application/graphql")

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf( "error executing request: %s", err)
	}
	return resp, nil
}

Die Funktion ist eigentlich recht einfach:

  • wir erstellen einen HTTP Client mit einem Timeout, damit die Anfrage abgebrochen wird, wenn es zu lange dauert
  • wir erstellen ein neues POST Request, dem wir neben der Art des Requests auch die URL und den Inhalt/body übermitteln. Da diese Funktion einen io.Reader erfordert wandeln wir unseren body in einen Buffer um der das io.Reader Interface implementiert
  • wir fügen dem Header unseres Requests das X-Shopify-Access-Token hinzu und definieren, dass das Request vom Typ application/graphql ist
  • anschließend führen wir das Request mit client.Do(req) aus
  • unterwegs behandeln wir noch Fehler, die eventuell auftreten können

2. Funktion zur Abfrage des Preises erstellen

Die 2. Funktion die wir schreiben soll unsere Query für den GraphQL Endpunkt bauen und das Ganze an unsere startrequest() Funktion übermitteln. Außerdem soll Sie das zurückgelieferte Ergebnis auswerten und an unser Hauptprogramm zurückliefern.

Das Anlegen eines eigenen Typs für das Einsortieren des JSON Antwort halte ich hier für Overkill, wir wollen ja nur einen Wert! Daher verwenden wir das Paket fastjson.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func getItemPrice(sku string) (float32, error) {
	query := `{
  inventoryItems(first:1, query:"sku:` + sku + `") {
    edges{
      node{
        sku
        variant {
          price
        }
      }
    }
  }
}`
	resp, err := startRequest([]byte(query))
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return 0, fmt.Errorf( "error reading response body: %s", err)
	}
	p := fastjson.GetString(body, "data", "inventoryItems", "edges", "0", "node", "variant", "price")
	if fastjson.Exists(body, "error") {
		return 0, fmt.Errorf("shopify responded with an error: %s", fastjson.GetString(body, "error"))
	}
	if fastjson.Exists(body, "data", "inventoryItems", "edges", "0", "node", "variant", "price") == false {
		return 0, fmt.Errorf("item could not be found in shopify: %s", sku)
	}
	price, err:= strconv.ParseFloat(p, 32)
	if err != nil {
		return 0, fmt.Errorf( "error reading response body: %s", err)
	}
	return float32(price), nil
}

Ich erkläre mal von oben nach unten was die Funktion macht:

  • zuerst legen wir die Query für den GraphQL Endpunkt fest. Diese ist im Grunde nur ein zusammengesetzter String in den zwischendurch die Artikelnummer eingefügt wird, die wir der Funktion beim Aufruf übergeben haben
  • danach übergeben wir unserer startRequest() die Query als byte Slice und erhalten die Antwort von Shopify als *http.Response zurück
  • wir sorgen mit defer resp.Body.Close() dafür, dass der Inhalt der Antwort nach Abschluss geschlossen wird und nutzen einen reader um den Inhalt in der Variable body zu speichern
  • jetzt holen wir uns über die Funktion fastjson.GetString() den Preis aus der Antwort von Shopify und erzeugen in der Zeile danach einen Fehler falls Shopify einen Fehler zurückmeldet.
  • außerdem behandeln wir noch den Fall, dass das Feld price von Shopify gar nicht zurückgeliefert wird (z.B. wenn Shopify den Artikel nicht kennt)
  • zuletzt machen wir aus dem String den uns Shopify liefert ein Float und geben den Preis zurück an die Hauptfunktion

Wie das mit fastjson funktioniert

Ich oben schnell darüber hinweggegangen wie genau wir mit fastjson unseren Preis erhalten. Im Grunde funktioniert das so, dass wir uns an der JSON antwort entlang hangeln: Die Antwort von Shopify sieht in etwa so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "data": {
    "inventoryItems": {
      "edges": [
        {
          "node": {
            "sku": "mySKU",
            "variant": {
              "price": "144.90"
            }
          }
        }
      ]
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 4,
      "actualQueryCost": 4,
      "throttleStatus": {
        "maximumAvailable": 1000.0,
        "currentlyAvailable": 996,
        "restoreRate": 50.0
      }
    }
  }
}

Unser fastjson.GetString() Aufruf sah ja so aus:

1
fastjson.GetString(body, "data", "inventoryItems", "edges", "0", "node", "variant", "price")

Wie man sieht, übergeben wir die komplette Antwort von Shopify durch die Variable body gefolgt von der Struktur, die wir in der JSON Antwort finden bis wir bei price landen. Das Einzige was man hier wirklich beachten muss ist, dass die edges in der Antwort eine Liste sind. Daher müssen wir fastjson mitteilen welches Element der Liste wir haben wollen. Wir haben hier ja nur ein Element, daher geben wir den Index 0 an: … “edges”, “0”, “node” …

Mit fastjson.Exists() kann man prüfen ob ein bestimmter Wert in der Antwort enthalten ist und entsprechend darauf reagieren. Natürlich hat fastjson noch weitere nützliche Funktionen. Mehr dazu unter:

https://github.com/valyala/fastjson

3. Die Hauptfunktion

Die Hauptfunktion ist relativ simpel. Wir übergeben *getItemPrice() die Artikelnummer, die wir suchen, prüfen ob es eventuell einen Fehler gab und geben ansonsten den Preis aus:

1
2
3
4
5
6
7
func main() {
	price, err := getItemPrice("your_SKU") // The SKU you are looking for
	if err != nil {
		log.Fatalf("could not get price from shopify: %s", err)
	}
	fmt.Println("Price:", price)
}

Wie schon gesagt, den kompletten Code findet Ihr unter:

https://gist.github.com/m3tam3re/98d9b57f404db3bc1ff8af133e54afd0

4. Das war’s 😇

Wie man sieht kann man sich relativ einfach Daten aus seinem Shopify Shop holen. Neben ein paar Programmierkenntnissen ist es natürlich hilfreich, wenn man sich mit GraphQL ein wenig auskennt.

Man könnte an diesem kleinen Programm berechtigterweise kritisieren, dass die Funktion getItemPrice() zu viele Aufgaben zu erledigen hat. In einem größeren Kontext hätte man wahrscheinlich das Erstellen der Query an eine eigene Funktion ausgelagert. Ich denke aber das Prinzip wird trotzdem klar.