Showing posts with label go. Show all posts
Showing posts with label go. Show all posts

Saturday, March 23, 2024

Copy Shopify Product Example

If you are working on a Shopify project, chances are you'll have multiple stores. One for development, one for pre-production and one for production or live which is accessible to the public. Because of this, you'ld probably want these stores to have the same products. Although there is an option to import/export products in Shopify admin, it does not copy over the metafields. And you're in deep equine effluence if rely heavily on metafields.

In this example, we are going to learn how to copy a product to another store using Shopify GraphQL. This example builds upon my previous blog, Shopify Bulk Query Example. I would recommend you read the previous blog if you want to know more about the cached products. I will only talk about how to copy a product to another store in this article. Okay, let's get to it.

Target Store

As mentioned above, we are building upon the previous example, Shopify Bulk Query Example. So we already have a Quickstart store in place. Let's create a target development store. On the Shopify partners page go to Stores -> Add store -> Create development store. Choose "Create a store to test and build", fill in the form as appropriate. Example Copy Product store:

After the target store has been created, it shouldn't have any products like below.

Custom App on Target Store

On the target store, go to Settings -> Apps and sales channels -> Develop apps. Allow custom app development and Create an app. Let's name it Product API. On the Configuration tab you must have the access scopes write_products and read_products. As of this writing, the webhook version is 2024-01. Install the app. On the API credentials tab, take note of your Admin API access token (you'll need to add this in the request header). Keep your access tokens secure. Only share them with developers that you trust to safely access your data. Take note of your API key and secret key as well. Our Copy Product store is now configured and ready to create products. For detailed steps on how to create a custom app, please take a look at my previous blog, Consuming Shopify Product API Example.

Config Changes

First off, we'll need to add the target store to our config file and update our code. Should be self explanatory based on the names. We have a source store and a target store. We copy the product from the source store (i.e. endpoint) over to the target store (i.e. targetEndpoint)

example-config.json


{
    "port": 0,
    "shopify": {
        "endpoint": "https://.myshopify.com/admin/api//graphql.json",
        "accessToken": "yourAccessToken",
        "targetEndpoint": "https://.myshopify.com/admin/api//graphql.json",
        "targetAccessToken": "targetAccessToken"
    }
}

config.go


// snipped...

type Shopify struct {
	Endpoint          string `json:"endpoint"`
	AccessToken       string `json:"accessToken"`
	TargetEndpoint    string `json:"targetEndpoint"`
	TargetAccessToken string `json:"targetAccessToken"`
}

// snipped...

Routing Addition

In main.go, we add the route to /copy-product which we will hit to trigger the copying of a product.

main.go


    // snipped...

    r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))
	r.Mount("/cached-products", router.GetCachedProducts(config, products))
	r.Mount("/copy-product", router.CopyProduct(config, products))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

Copy Shopify Product Handler

In this part of the code, we pull the gid of the product that we are going to copy from the query parameter. We then find it if that product exists in the product cache. We call the CopyProduct function which returns an error if the copy was unsuccessful and then we send back a Copy product failed message to the caller in JSON format. For a successful copy, the source product information is returned.

products.go


// snipped...

func CopyProduct(config config.Config, products []service.Product) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {

		gid := r.URL.Query().Get("gid")

		copyProduct := service.Product{
			ID:          "",
			Title:       "",
			Handle:      "",
			Vendor:      "",
			ProductType: "",
			Tags:        make([]string, 0),
			Metafields:  make([]service.Metafield, 0),
		}

		for _, product := range products {
			if product.ID == gid {
				fmt.Println("Found:", product.ID)
				copyProduct = product
				break
			}
		}

		jsonBytes := marshaller.Marshal(copyProduct)

		err := service.CopyProduct(config, copyProduct)
		if err != nil {
			jsonBytes = []byte("{ \"message\": \"Copy product failed\"}")
		}

		w.Header().Set(contentType, applicationJson)
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

Copy Shopify Product Crux

Nothing fancy. Just an update on the data model based on Shopify documentation. For more information, go to Shopify Docs.

bulk-query.go


// snipped...

type ProductCreate struct {
	Product Product `json:"product,omitempty"`
}

type Data struct {
	BulkOperationRunQuery BulkOperationRunQuery `json:"bulkOperationRunQuery,omitempty"`
	CurrentBulkOperation  BulkOperation         `json:"currentBulkOperation,omitempty"`
	ProductCreate         ProductCreate         `json:"productCreate,omitempty"`
}

// snipped...

This is where all the grunt work happens. The first lines of the copy product function will create the metafields portion of the GraphQL query. The metafields portion is then inserted to the productCreate GraphQL mutation query. For this example, we are going to assume all metafields are of type single_line_text_field. Because of this, the caveat is, this will not work if the metafield of the product you are going to copy is of a different type. The request is then sent to the target endpoint with the target accss token in the header. If all goes well, it will print the gid of the newly created product. Otherwise the error is passed to the caller to handle.

copy-product.go


package service

import (
	"fmt"
	"io"
	"net/http"
	"shopify-product-api/config"
	"shopify-product-api/marshaller"
	"strings"
)

func CopyProduct(config config.Config, product Product) error {
	// https://shopify.dev/docs/api/admin-graphql/2024-01/mutations/productCreate
	fmt.Println("++ copy product")

	metafieldStr := ""
	var sb strings.Builder

	for i := 0; i < len(product.Metafields); i++ {
		if i > 0 {
			sb.WriteString(",")
		}
		sb.WriteString("{")
		sb.WriteString(fmt.Sprintf(`namespace: "%s",`, product.Metafields[i].Namespace))
		sb.WriteString(fmt.Sprintf(`key: "%s",`, product.Metafields[i].Key))
		sb.WriteString(fmt.Sprintf(`value: "%s",`, product.Metafields[i].Value))
		sb.WriteString(fmt.Sprintf(`type: "%s"`, "single_line_text_field")) // assume all metafields are of this type
		sb.WriteString("}")
	}

	if len(product.Metafields) > 0 {
		metafieldStr = fmt.Sprintf(`metafields: [%s]`, sb.String())
	}

	productCreateGql := fmt.Sprintf(`
	mutation { 
		productCreate(
			input: {
				title: "%s",
				productType: "%s",
				vendor: "%s",
				tags: "%s",
				%s
			}
		) {
			product {
				id
			}
		}
	}
	`, product.Title, product.ProductType,
		product.Vendor, strings.Join(product.Tags, ","),
		metafieldStr)

	query := GqlQuery{
		Query: productCreateGql,
	}

	client := &http.Client{}

	responseBody, err := sendCopyRequest(client, query, config)
	if err != nil {
		return err
	}

	gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

	fmt.Println("New product copied: ", gqlResp.Data.ProductCreate.Product.ID)

	return nil
}

func sendCopyRequest(client *http.Client, query GqlQuery, config config.Config) ([]byte, error) {
	q := marshaller.Marshal(query)
	body := strings.NewReader(string(q))

	req, err := http.NewRequest(http.MethodPost, config.Shopify.TargetEndpoint, body)
	if err != nil {
		return nil, err
	}
	req.Header.Add(contentType, applicationJson)
	req.Header.Add("X-Shopify-Access-Token", config.Shopify.TargetAccessToken)

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	fmt.Println("Response status:", resp.Status)

	return io.ReadAll(resp.Body)
}

Demonstration

I'm assuming you have read my previous blogs, Consuming Shopify Product API Example and Shopify Bulk Query Example. So you should be able to run the golang app and hit the endpoints with Postman. First, let's fetch the cached products and I've chosen gid://shopify/Product/8787070452004 as my source product like so.

Second, let's hit the copy product endpoint and provide the gid query parameter. A successful copy returns the source product information like so.

On the logs, we should see something like below.


Found: gid://shopify/Product/8787070452004
++ copy product
Response status: 200 OK
New product copied:  gid://shopify/Product/8343228219651
2024/03/16 22:35:45 "GET http://localhost:4000/copy-product?gid=gid://shopify/Product/8787070452004 HTTP/1.1" from [::1]:51397 - 200 283B in 22.7344888s

Before we can see the metafields on the Shopify admin product page, we need some bit of configuration for the metafields to show on the product page. Go to Settings -> Custom Data -> Products -> Metafields without a definition. Our namespace and key have been created. Add the definition (i.e. name, description and type). After saving, it will appear in the product page.

Finally, we can see our copied product along with the tags and metafields in all its glory.

Copy Shopify Product Wrap Up

There you have it. A way to copy a product from store to store in Shopify using Shopify GraphQL Admin API. This should form as a base for copying products between stores. Grab the full repo here, github.com/jpllosa/shopify-product-api/tree/copy-product.

Monday, December 25, 2023

Shopify Bulk Query Example

When should I use bulk query? Read on and let's analyse it together.

This blog builds upon my previous blog, Consuming Shopify Product API Example. We'll compare the bulk query with the non-bulk query we did on that example.

Shopify Bulk Query Changes

These are the changes we made to the previous example so we can do Shopify bulk query.

main.go


// snipped...

func main() {
	config, err := config.Load("./conf/config.json")
	if err != nil {
		panic(err)
	}

	products := service.BulkQuery(config)

	r := chi.NewRouter()
	r.Use(middleware.Logger)

	r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))
	r.Mount("/cached-products", router.GetCachedProducts(config, products))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

Just a few changes here. We created a new package named service to handle the bulk query operations and a new path to hit to pull the cached products. Quick and easy.

model.go


package service

type Node struct {
	ID          string   `json:"id,omitempty"`
	Title       string   `json:"title,omitempty"`
	Handle      string   `json:"handle,omitempty"`
	Vendor      string   `json:"vendor,omitempty"`
	ProductType string   `json:"producType,omitempty"`
	Tags        []string `json:"tags,omitempty"`
	Namespace   string   `json:"namespace,omitempty"`
	Key         string   `json:"key,omitempty"`
	Value       string   `json:"value,omitempty"`
	ParentID    string   `json:"__parentId,omitempty"`
}

type Product struct {
	ID          string      `json:"id,omitempty"`
	Title       string      `json:"title,omitempty"`
	Handle      string      `json:"handle,omitempty"`
	Vendor      string      `json:"vendor,omitempty"`
	ProductType string      `json:"producType,omitempty"`
	Tags        []string    `json:"tags,omitempty"`
	Metafields  []Metafield `json:"metafields,omitempty"`
}

type Metafield struct {
	Namespace string `json:"namespace,omitempty"`
	Key       string `json:"key,omitempty"`
	Value     string `json:"value,omitempty"`
	ParentID  string `json:"__parentId,omitempty"`
}

Here we model the data types we use.

products.go


// snipped...
func GetCachedProducts(config config.Config, products []service.Product) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {

		jsonBytes := marshaller.Marshal(products)

		w.Header().Set(contentType, applicationJson)
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

A function that handles requests to the new /cached-products path.

bulk-query.go


// snipped...
func BulkQuery(config config.Config) []Product {
	fmt.Println("++ bulk query")
	bulkQueryGql := fmt.Sprintf(`
	mutation {
		bulkOperationRunQuery(
			// ... snipped ...
		}
	}
	`)

	query := GqlQuery{
		Query: bulkQueryGql,
	}

	client := &http.Client{}

	responseBody, err := sendRequest(client, query, config)
	if err != nil {
		panic(err)
	}

	gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

	if gqlResp.Data.BulkOperationRunQuery.BulkOperation.Status == "CREATED" {
		fmt.Println("Created at: ", gqlResp.Data.BulkOperationRunQuery.BulkOperation.CreatedAt)
		currentOperationQueryGql := fmt.Sprintf(`
		query CurrentBulkOperation {
			currentBulkOperation {
				completedAt
				createdAt
				errorCode
				fileSize
				id
				objectCount
				status
				url
			}
		}
		`)

		query = GqlQuery{
			Query: currentOperationQueryGql,
		}

		for {
			time.Sleep(time.Second * 2)

			responseBody, err := sendRequest(client, query, config)
			if err != nil {
				panic(err)
			}

			gqlResp = marshaller.Unmarshal[GqlResponse](responseBody)

			if gqlResp.Data.CurrentBulkOperation.Status == "CANCELED" ||
				gqlResp.Data.CurrentBulkOperation.Status == "CANCELING" ||
				gqlResp.Data.CurrentBulkOperation.Status == "EXPIRED" ||
				gqlResp.Data.CurrentBulkOperation.Status == "FAILED" {
				fmt.Println("Status: ", gqlResp.Data.CurrentBulkOperation.CreatedAt)
				break
			}

			if gqlResp.Data.CurrentBulkOperation.Status == "COMPLETED" {
				fmt.Println("URL: ", gqlResp.Data.CurrentBulkOperation.URL)
				productFile, err := downloadFile("products.tmp", gqlResp.Data.CurrentBulkOperation.URL)
				if err != nil {
					break
				}
				return parseProductsFile(productFile)
			}
		}
	}

	return make([]Product, 0)
}
// snipped...

This is where all the magic happens. We issue a bulk query request via mutation operation. We then unmarshall the response and check the bulk operation status if it has been created. If it was created, we then poll the current bulk operation until it is completed, canceled, failed, etc. Once it is completed, we download it and save it into a temporary file. This file will be in JSONL (JSON Lines) format, then we will have to parse the file in order to build the product tree.

There is also a webhook way of checking the bulk operation status. It is recommended over polling as it limits the number of redundant API calls. But for the purposes of this example, we'll do polling.

For more details about Shopify bulk operations, go to Perform bulk operations with the GraphQL Admin API. Go to Bulk Operation Status to learn more valid status values.

The JSONL file would look something like below:


{"id":"gid:\/\/shopify\/Product\/8787070189860","title":"The Videographer Snowboard","handle":"the-videographer-snowboard","vendor":"Quickstart (5cec88e7)","productType":"","tags":[]}
{"id":"gid:\/\/shopify\/Product\/8787070222628","title":"The Minimal Snowboard","handle":"the-minimal-snowboard","vendor":"Quickstart (5cec88e7)","productType":"","tags":[]}
{"id":"gid:\/\/shopify\/Product\/8787070386468","title":"The Archived Snowboard","handle":"the-archived-snowboard","vendor":"Snowboard Vendor","productType":"","tags":["Archived","Premium","Snow","Snowboard","Sport","Winter"]}

Running the Shopify Bulk Query Example

On start up, you should see somethingl like below. Shopify has returned with a download URL. Which means the bulk query has completed and we are ready to serve the cached products.

Comparison with Non-Bulk Query

Now let's compare the new way of pulling data to the old way.

Can you spot the difference? In terms of speed, the response time of the new way was clearly super fast. Just 4ms compared to 279ms, imagine that? What's more is that not only did the old way take longer, it returned less data. It return 639 B compared to 4.65 KB. In other words, in the old way we only received 3 products while in the new way we received all products including metafields. That's an icing on the cake. As for start up time of the app, it was negligible.

Shopify Bulk Query Wrap Up

Would you do bulk query now or not? It is up to you to identify a potential bulk query. Queries that use pagination to get all pages of results are the most common candidates. There are limitations on a bulk query though. For example, you can't pull data that's nested two levels deep. Check the Shopify documentation for more information.

There you have it. Another way to pull product data from the Shopify GraphQL Admin API. If you got a better way of doing things (e.g. how to parse the JSONL better), just raise a pull request. Happy to look at it. Grab the repo here, github.com/jpllosa/shopify-product-api, it's on the bulk-query branch.

Sunday, November 19, 2023

Go TLS MySQL Example

Nowadays, it is vital to keep data secure. If your connection to a database goes through an untrusted network, it is prudent to encrypt data going through the wire. Fortunately, we can make use of MySQL's internal SSL (Secure Sockets Layer) support to make the connection secure. Read on and learn how to secure a connection to MySQL in Golang.

SSL is a standard technology for securing internet connection by encrypting data sent between a client and server. SSL and TLS (Transport Layer Security) are sometimes used interchangeably but TLS is an updated, more secure version of SSL. TLS fixes existing SSL vulnerabilities.

Creating SSL Certificates

The Common Name value used for the server and client certificates/keys must each differ from the Common Name value used for the CA certificate. On my Windows 10 machine, I'm using OpenSSL 3.1.2 1 Aug 2023 to generate the below certificates and keys. Create a clean directory and generate the certificates and keys (e.g. mkdir pems).

Create CA certificate

  1. openssl genrsa 2048 > my-ca-key.pem
  2. openssl req -new -x509 -nodes -days 3600 -key my-ca-key.pem -out my-ca.pem -addext "subjectAltName = DNS:localhost, IP:127.0.0.1"

$ openssl req -new -x509 -nodes -days 3600 -key my-ca-key.pem -out my-ca.pem -addext "subjectAltName = DNS:localhost, IP:127.0.0.1"
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:CA
Email Address []:

Check the contents of the certificate, openssl x509 -in my-ca.pem -text.


$ openssl x509 -in my-ca.pem -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            59:76:52:41:c9:d8:7a:d2:51:2a:05:3a:a6:f9:d8:3e:9d:a3:3d:a3
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = CA
        Validity
            Not Before: Nov  5 09:49:14 2023 GMT
            Not After : Sep 13 09:49:14 2033 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:a8:cc:c5:14:d9:ef:90:07:43:81:b1:80:f7:42:
                    ...snipped...
                    16:75:45:05:69:5a:73:24:b3:f2:93:cb:5f:3b:8f:
                    31:15
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                B6:1A:8E:CA:32:F7:AF:A9:35:EF:27:5F:BF:DE:BA:A2:4B:66:F4:39
            X509v3 Authority Key Identifier:
                B6:1A:8E:CA:32:F7:AF:A9:35:EF:27:5F:BF:DE:BA:A2:4B:66:F4:39
            X509v3 Basic Constraints:
                CA:TRUE
            X509v3 Subject Alternative Name:
                DNS:localhost, IP Address:127.0.0.1
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        04:7a:75:59:80:fb:85:58:76:4a:c8:4d:69:d9:d3:72:42:fd:
		...snipped...

Create server certificate, key, and sign it. Create extfile.cnf

  1. openssl req -newkey rsa:2048 -nodes -keyout my-server-key.pem -out my-server-req.pem
  2. openssl rsa -traditional -in my-server-key.pem -out my-server-key.pem
  3. openssl x509 -req -in my-server-req.pem -days 3600 -CA my-ca.pem -CAkey my-ca-key.pem -set_serial 01 -out my-server-cert.pem -extfile extfile.cnf
In this example, I was using MySQL v5.7.21 which did not support the PKCS #8 format for the key so I had to add the -traditional option to make the key in PKCS #1 format. On a later version of MySQL, I didn't need to use the -traditional option (e.g. MySQL v5.7.33). Simply put, for PKCS #1 (Public-Key Cryptography Standard), your key would start with something like --- BEGIN RSA PRIVATE KEY --- and end with --- END RSA PRIVATE KEY ---. For PKCS #8, it's --- BEGIN PRIVATE KEY --- and end with --- END PRIVATE KEY ---. Please google it for more details.

$ openssl req -newkey rsa:2048 -nodes -keyout my-server-key.pem -out my-server-req.pem
.......+.+..............+......+...+..........+...........+....+...+...+.....+.......+..+.+.....+.+.....+......+....+......+..+......+....+....................+.......+.........+..+++++++++++++++++++++++++++++++++++++++*.+.+............+..+...+...+.+...+.....+..................+.........+.+.....+...+.+++++++++++++++++++++++++++++++++++++++*...+.....+....+.....+....+.....+.+..............+.......+...+...+..............+.+..+...............+...+.......+.....+......+.......+...+..+...+.........+..........+.....+.+..............+.......+........................+..+...+....+..+.+........+....+..+.+...+...............+......+........+.......+...........+...+....+...+...+............+..+...+...+.......+........+.+......+............+..+.+.....+....+.....+...+.+..+..................................+......+..+...+....+........+...+.......+........+...................+...........+...+.........+...+.......+...+..+......+.+.....+.+..............+......+.............+.....+.........+............+..........+.........+.........+......+...........+.+..+.+.....+.......+.........+......+...+.....+......+.+..+...+.......+......+.....+.......+.....+..........+..+............+...+.+..+....+......+..+.............+...+.....+......+..........+.........+.........+.....+...+.......+......+.....+......+........................+.+..............+....+..+..................+...+...+.+......+..+.+..+.........+.+..............+...+...+...+.......+........+.+......+.....+...+....+........+....+..................+...+...+..+.......+...+..................+.....+....+.....+...+....+.....+....+..+.......+............+.....+.........+.+......+...+......+...+...+..+.............+........+....+..+.............+..+.............+..+....+.........+..+.........+....+...+..+...+...+.+......+..+.+...........+......+....+...........+...+.......++++++
...+...+..+............+...+..........+...+...........+.+..+............+.+...+...........+.+..+..........+...........+....+.....+.+......+++++++++++++++++++++++++++++++++++++++*........+...+..+...+....+.....+....+......+........+.......+.....+...................+++++++++++++++++++++++++++++++++++++++*........+.......+......+.....+.+.....++++++
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:Server
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

$ openssl rsa -traditional -in my-server-key.pem -out my-server-key.pem
writing RSA key

Create extfile.cnf with the following contents.


subjectAltName = DNS:localhost, IP:127.0.0.1

$ openssl x509 -req -in my-server-req.pem -days 3600 -CA my-ca.pem -CAkey my-ca-key.pem -set_serial 01 -out my-server-cert.pem -extfile extfile.cnf
Certificate request self-signature ok
subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = Server

Check the contents of the certificate, openssl x509 -in my-server-cert.pem -text.


$ openssl x509 -in my-server-cert.pem -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = CA
        Validity
            Not Before: Nov  5 09:54:01 2023 GMT
            Not After : Sep 13 09:54:01 2033 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = Server
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:bd:36:70:95:7e:6e:e6:09:5d:34:f8:42:8d:ef:
                    ...snipped...
                    61:0e:10:12:80:8b:90:3b:e2:d2:d7:e0:c8:ba:c0:
                    ea:97
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:localhost, IP Address:127.0.0.1
            X509v3 Subject Key Identifier:
                EA:77:57:2D:B2:62:64:A5:FB:7C:71:7D:1E:00:C9:8D:71:6F:91:68
            X509v3 Authority Key Identifier:
                B6:1A:8E:CA:32:F7:AF:A9:35:EF:27:5F:BF:DE:BA:A2:4B:66:F4:39
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        a2:c4:99:58:a5:72:ad:3f:79:b4:e6:3e:e5:11:fd:8f:fd:9a:
		...snipped...

Create client certificate, key, and sign it.

  1. openssl req -newkey rsa:2048 -nodes -keyout my-client-key.pem -out my-client-req.pem
  2. openssl rsa -traditional -in my-client-key.pem -out my-client-key.pem
  3. openssl x509 -req -in my-client-req.pem -days 3600 -CA my-ca.pem -CAkey my-ca-key.pem -set_serial 01 -out my-client-cert.pem

$ openssl req -newkey rsa:2048 -nodes -keyout my-client-key.pem -out my-client-req.pem
.....+....+...+......+..+++++++++++++++++++++++++++++++++++++++*...........+...+.+......+.................+.+..+++++++++++++++++++++++++++++++++++++++*..+....+..+.............+.....+......+.+..+.......+......+...............+........+.+.....+.+...+............+..+................+..............+.......+......+.........+.....+...+.......+...+..+.........+.+.....+....+.........+..+...+.......+........+...............+...+...+.+......+........+.+.....+.............+...+..+......+.......+.....+...+.......+..+.......+...............+...........+.......+.....+.+.................+....+........+.............+...+..+...+............+.......+..+......+...+....+...+..+......+.+.....+..........+........+.+..+.......+........+...+......+......+.............++++++
.....+...+.+..+++++++++++++++++++++++++++++++++++++++*...+....+...........+.........+.+.....+.........+....+..+......+++++++++++++++++++++++++++++++++++++++*.+...+.....+......+.............+..+.+..............+.+...+...........+...+...+....+.....+...+.......+...+........+...+.+...+............+...........++++++
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:Client
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

$ openssl rsa -traditional -in my-client-key.pem -out my-client-key.pem
writing RSA key

$ openssl x509 -req -in my-client-req.pem -days 3600 -CA my-ca.pem -CAkey my-ca-key.pem -set_serial 01 -out my-client-cert.pem
Certificate request self-signature ok
subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = Client

Check the contents of the certificate, openssl x509 -in my-client-cert.pem -text.


$ openssl x509 -in my-client-cert.pem -text
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = CA
        Validity
            Not Before: Nov  5 09:59:50 2023 GMT
            Not After : Sep 13 09:59:50 2033 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = Client
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:a9:f1:74:07:eb:fa:96:d1:0f:c6:f0:32:c6:c6:
					...snipped...

At the end of all those commands, you should have something like below in your pems folder. Verify your certificates like below.


$ ls
extfile.cnf    my-ca.pem           my-client-key.pem  my-server-cert.pem  my-server-req.pem
my-ca-key.pem  my-client-cert.pem  my-client-req.pem  my-server-key.pem

$ openssl verify -CAfile my-ca.pem my-server-cert.pem my-client-cert.pem
my-server-cert.pem: OK
my-client-cert.pem: OK

Hooking Up the PEMs to MySQL

Copy my-ca.pem, my-server-cert.pem, and my-server-key.pem to your MySQL data directory. Run MySQL like so mysqld --console --ssl-ca=my-ca.pem --ssl-cert=my-server-cert.pem --ssl-key=my-server-key.pem and you should have something like below. The below tells us that MySQL is able to read our CA certificate and we should be able to do SSL connections.


2023-11-05T22:14:14.749469Z 0 [Note] Plugin 'FEDERATED' is disabled.
2023-11-05T22:14:14.751171Z 0 [Note] InnoDB: Loading buffer pool(s) from D:\mysql-5.7.21-winx64\data\ib_buffer_pool
2023-11-05T22:14:15.205534Z 0 [Warning] CA certificate my-ca.pem is self signed.
2023-11-05T22:14:15.218988Z 0 [Note] Server hostname (bind-address): '*'; port: 3306
2023-11-05T22:14:15.219894Z 0 [Note] IPv6 is available.
2023-11-05T22:14:15.220471Z 0 [Note]   - '::' resolves to '::';
2023-11-05T22:14:15.224511Z 0 [Note] Server socket created on IP: '::'.
2023-11-05T22:14:15.301522Z 0 [Note] Event Scheduler: Loaded 0 events
2023-11-05T22:14:15.302306Z 0 [Note] mysqld: ready for connections.
Version: '5.7.21'  socket: ''  port: 3306  MySQL Community Server (GPL)
2023-11-05T22:14:15.512362Z 0 [Note] InnoDB: Buffer pool(s) load completed at 231105 22:14:15

You can also set SSL to be database wide. Just set the MySQL config file like below. For this exercise we'll try setting SSL connections per user at the moment. When you can successfully connect then you can easily move up to securing it database wide. I'd recommend on a production system to secure it database wide.


ssl_ca=my-ca.pem
ssl_cert=my-server-cert.pem
ssl_key=my-server-key.pem
require_secure_transport=ON

Trusting the Certificates

You may or may not have to do this bit. For MySQL v5.7.21 on my Windows 10 Pro 19045 build and Go v1.18.3, I didn't have to do this. But on my other Windows 10 machine running MySQL v5.7.33, Go v1.18.2, I had to make the target machine trust the self-signed certificates by manually importing it using Microsoft Management Console (MMC). Start command prompt as Administrator then run mmc. Google how to manually import self-signed certificates for more details. In summary, I had to manually import the certificates under Console Root > Certificates > Personal > Certificates.

Testing the Connection

For this example, I was on MySQL Workbench v6.3. I should be on v8 really but it's on my other machine. Anyway, add your PEM (Privacy Enhanced Mail) files in the appropriate boxes. PEM files contain the public certificate or may include the entire certificate chain including public key, private key and root certificates.

For a successful test connection, you should have something like below:

Secure the User

I'm utilizing my past article, Accessing MySQL using Go Example. Granting you followed that example, then you most likely have a username "golang" that you have used to connect to the database. Let us make golang do a secure connection. Run the SQL query to require SSL on golang


ALTER USER golang@localhost REQUIRE SSL;

To remove SSL on golang, run.


ALTER USER golang@localhost REQUIRE NONE;

Check what TLS version our MySQL v5.7.21 server supports. As you can see below, it only supports TLSv1 and TLSv1.1. Our Go crypto/tls library supports TLSv1.3 by default. We'll need to configure our connection to go down a couple of versions lower. Otherwise we'll have an error, something like this, tls: server selected unsupported protocol version 302.


SHOW GLOBAL VARIABLES LIKE 'tls_version';

Coding Time

I've made changes to the original connection implementation from Accessing MySQL using Go Example and I patterned the TLS connection from the MySQL Golang RegisterTLSConfig API documentation.


// ... code snipped...

rootCertPool := x509.NewCertPool()
pem, err := ioutil.ReadFile("../pems/my-ca.pem")
if err != nil {
	log.Fatalf("configuration: reading CA pem file: %v", err)
}
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
	log.Fatalf("configuration: failed to append pem file: %v", err)
}
clientCert := make([]tls.Certificate, 0, 1)
certs, err := tls.LoadX509KeyPair("../pems/my-client-cert.pem", "../pems/my-client-key.pem")
if err != nil {
	log.Fatalf("configuration: failed to load key pair: %v", err)
}
clientCert = append(clientCert, certs)
mysql.RegisterTLSConfig("secure", &tls.Config{
	RootCAs:      rootCertPool,
	Certificates: clientCert,
	MinVersion:   tls.VersionTLS10, //without this defaults tls1.3 which not supported by our mysql
	MaxVersion:   tls.VersionTLS11,
})
cfg.TLSConfig = "secure"

// ... code snipped ...

fmt.Println("Securely connected!")

rows, err := db.Query("SELECT * FROM album")
if err != nil {
	fmt.Errorf("database query: %v", err)
}
defer rows.Close()

fmt.Printf("%2s %15s %15s %6s \n", "ID", "Title", "Artist", "Price")
for rows.Next() {
	var alb Album
	err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price)
	if err != nil {
		fmt.Errorf("row scan: %v", err)
	} else {
		fmt.Printf("%2d %15s %15s %6.2f \n", alb.ID, alb.Title, alb.Artist, alb.Price)
	}
}

The notable difference from the API documentation is the addition of the version support by our MySQL server. On another note, if you followed the Creating SSL Certificates and Keys Using openssl from MySQL's documentation, chances are, you didn't specify a subjectAltName. You'll possibly get an IP SANS error when connecting. I later found out that you can just add a server name in tls.Config and it should connect. Somelike below:


mysql.RegisterTLSConfig("secure", &tls.Config{
	RootCAs:      rootCertPool,
	Certificates: clientCert,
	MinVersion:   tls.VersionTLS10,
	MaxVersion:   tls.VersionTLS11,
	ServerName:   "InsertYourServerNameHere",
})

After we are connected, we query the database and print out the records in a neat table format. Running the code above in VS Code via launch.json, you should have something like below:


DAP server listening at: 127.0.0.1:53186
Type 'dlv help' for list of commands.
Securely connected!
ID           Title          Artist  Price
 1      Blue Train   John Coltrane  56.99 
 2     Giant Steps   John Coltrane  63.99 
 3            Jeru  Gerry Mulligan  17.99 
 4   Sarah Vaughan   Sarah Vaughan  34.98
Process 6508 has exited with status 0
Detaching

Go TLS MySQL Wrap Up

There you have it. Hope you enjoyed reading and trying out the example as much as I did. The complete project can be cloned from github.com/jpllosa/go-relational-database/tree/tls-config. This piece of code is under the tls-config branch.

Saturday, October 28, 2023

Consuming Shopify Product API Example

In this blog, we are going to learn how to pull product data from the Shopify API. To build upon previous examples like Starting Out on the go-chi Framework and GraphQL with Postman, we will implement this in Go using the go-chi framework and hit the Shopify GraphQL endpoint. Let's begin.

User Story

As a user, I want to retrieve my Shopify products in JSON format, so I can use it in my analytics software.

Shopify Setup

First off, I will not go into too much detail in setting Shopify up. I will let the Shopify documentation do that for me. To get this example running, you'll need a Shopify Partner account. When I created my Shopify partner account, it was free and I don't see any reason that it won't be in the future. Once you have a pertner account, create a development store or use the quickstart store with test data preloaded.

Go into your store, then Settings, then Apps and sales channels, then Develop apps and then Create an app.

On the Configuration tab you must have the access scopes write_products and read_products. As of this writing, the webhook version is 2023-10.

On the API credentials tab, take note of your Admin API access token (you'll need to add this in the request header). Keep your access tokens secure. Only share them with developers that you trust to safely access your data. Take note of your API key and secret key as well.

There you have it. We are all set on the Shopify configuration end.

Testing the Shopify API

Before we go straight into coding, let's test the API call first. Fire up Postman and let's hit the GraphQL endpoint. The URL is of the form https://<storename>.myshopify.com/admin/api/<version>/graphql.json. Add X-Shopify-Access-Token in your header and the value is your Admin API access token. We'll do a query for the first 5 products. The image below shows we can happily make a request. Go to GraphQL Admin API reference for more details.


query {
  products(first: 5, reverse: false) {
    edges {
      node {
        id
        title
        handle
        vendor
        productType
        tags
      }
    }
  }
}

The Main

main.go


// snipped...

func main() {
	config, err := config.Load("./conf/config.json")
	if err != nil {
		panic(err)
	}

	r := chi.NewRouter()
	r.Use(middleware.Logger)

	r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

The first thing the program does is load up the configuration. As mentioned above, we are building upon a previous example, Starting Out on the go-chi Framework. We utilize the go-chi framework for routing incoming HTTP reqeusts. Any requests to the root resource will return a status in JSON format. Any requests to the /products resource will return a list of products in JSON format. I will not expand on router.GetStatus because I have explained it in my previously mentioned blog, Starting Out on the go-chi Framework. I just moved it into a new package making it more organized.

Configuration

config.go


// snipped...

type Config struct {
	Port    int     `json:"port"`
	Shopify Shopify `json:"shopify"`
}

type Shopify struct {
	Endpoint    string `json:"endpoint"`
	AccessToken string `json:"accessToken"`
}

func Load(filePath string) (Config, error) {
	config, err := unmarshalFile[Config](filePath)
	if err != nil {
		return Config{}, err
	}

	return config, nil
}

func unmarshalFile[T any](filePath string) (T, error) {
	pointer := new(T)

	f, err := os.Open(filePath)
	if err != nil {
		return *pointer, fmt.Errorf("opening config file: %w", err)
	}
	defer f.Close()

	jsonBytes, err := io.ReadAll(f)
	if err != nil {
		return *pointer, fmt.Errorf("reading config file: %w", err)
	}

	err = json.Unmarshal(jsonBytes, &pointer)
	if err != nil {
		return *pointer, fmt.Errorf("unmarshalling config file: %w", err)
	}

	return *pointer, nil
}

Making the configuration file in JSON format will make it easy for us to use. We simply read the file and unmarshal the byte array into our Config type and bob's your uncle. Then we easily get the value accessing the fields as seen in main.go(e.g. fmt.Sprintf(":%v", config.Port)).

JSON Marshaller

json.go


func Marshal(v any) []byte {
	jsonBytes, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}

	return jsonBytes
}

func Unmarshal[T any](data []byte) T {
	var result T

	err := json.Unmarshal(data, &result)
	if err != nil {
		panic(err)
	}

	return result
}

Nothing special with this file. It is just a wrapper of the standard JSON Go library. Didn't want to muddy the resource handlers with JSON error handling.

Get Products

products.go


// snipped...
type GqlQuery struct {
	Query string `json:"query"`
}

type GqlResponse struct {
	Data       Data       `json:"data,omitempty"`
	Extensions Extensions `json:"extensions,omitempty"`
}

type Data struct {
	Products Products `json:"products,omitempty"`
}

type Products struct {
	Edges []Edge `json:"edges,omitempty"`
}

type Edge struct {
	Node Node `json:"node,omitempty"`
}

type Node struct {
	Id          string   `json:"id,omitempty"`
	Title       string   `json:"title,omitempty"`
	Handle      string   `json:"handle,omitempty"`
	Vendor      string   `json:"vendor,omitempty"`
	ProductType string   `json:"productType,omitempty"`
	Tags        []string `json:"tags,omitempty"`
}

type Extensions struct {
	Cost Cost `json:"cost,omitempty"`
}

type Cost struct {
	RequestedQueryCost int            `json:"requestedQueryCost,omitempty"`
	ActualQueryCost    int            `json:"actualQueryCost,omitempty"`
	ThrottleStatus     ThrottleStatus `json:"throttleStatus,omitempty"`
}

type ThrottleStatus struct {
	MaximumAvailable   float32 `json:"maximumAvailable,omitempty"`
	CurrentlyAvailable int     `json:"currentlyAvailable,omitempty"`
	RestoreRate        float32 `json:"restoreRate,omitempty"`
}

func GetProducts(config config.Config) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {
		productsGql := fmt.Sprintf(`{
			products(first: 3, reverse: false) {
			  edges {
				node {
				  id
				  title
				  handle
				  vendor
				  productType
				  tags
				}
			  }
			}
		  }`)

		query := GqlQuery{
			Query: productsGql,
		}

		q := marshaller.Marshal(query)
		body := strings.NewReader(string(q))

		client := &http.Client{}
		req, err := http.NewRequest(http.MethodPost, config.Shopify.Endpoint, body)
		if err != nil {
			panic(err)
		}
		req.Header.Add("Content-Type", "application/json")
		req.Header.Add("X-Shopify-Access-Token", config.Shopify.AccessToken)

		resp, err := client.Do(req)
		if err != nil {
			panic(err)
		}
		defer resp.Body.Close()

		fmt.Println("Response status:", resp.Status)

		responseBody, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			panic(err)
		}

		gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

		jsonBytes := marshaller.Marshal(gqlResp.Data.Products.Edges)

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

Welcome to the heart of this article. We start by creating the type struct of the GraphQL response so we can easily unmarshal it. As you saw earlier in Testing the Shopify API, the Postman image shows a snapshot of the GraphQL response from Shopify. When GetProducts is called, it starts by creating the GraphQL query for products. Marshal the query into a byte array then passes it into a new HTTP request. We create an HTTP client so we can add in the Shopify Access Token in the header and the content type. We then send the request by calling client.Do(req) and read the response. Converting the response into a byte array so we can unmarshal it. After that we strip out the stuff we don't need. We only need the Nodes. Once we have the nodes, we convert it into bytes and send it back. That is all there is to it. Oh yeah, don't forget to close response after using it.

Output

I've set my port to 4000 so hitting http://localhost:4000 will return a response like below:


{
    "id": 1,
    "message": "API ready and waiting"
}

Hitting http://localhost:4000/products will return a response like below:


[
    {
        "node": {
            "id": "gid://shopify/Product/8787070189860",
            "title": "The Videographer Snowboard",
            "handle": "the-videographer-snowboard",
            "vendor": "Quickstart (5cec88e7)"
        }
    },
    {
        "node": {
            "id": "gid://shopify/Product/8787070222628",
            "title": "The Minimal Snowboard",
            "handle": "the-minimal-snowboard",
            "vendor": "Quickstart (5cec88e7)"
        }
    },
    {
        "node": {
            "id": "gid://shopify/Product/8787070386468",
            "title": "The Archived Snowboard",
            "handle": "the-archived-snowboard",
            "vendor": "Snowboard Vendor",
            "tags": [
                "Archived",
                "Premium",
                "Snow",
                "Snowboard",
                "Sport",
                "Winter"
            ]
        }
    }
]

Consuming Shopify Product API Wrap Up

There you have it. A way to pull data from the Shopify GraphQL Admin API and spitting it out in a slightly different format. In between we utilized the go-chi framework for routing requests. We used the standard Go library for JSON manipulation, HTTP communication and file access. Lastly, did we satisfy the user story? Grab the full repo here, github.com/jpllosa/shopify-product-api.

Tuesday, October 4, 2022

Starting Out on the go-chi Framework Example

Picture this. The company you work for has decided to move from Java to Golang to create microservices. What would you do? As for me, I went straight into creating an HTTP GET endpoint that returns a nice "hello world" message. As you'll see below, the code uses the go chi framework. Why the go-chi framework? I don't really know but maybe because it was recommended by the long time IT provider of the company. Based on the quickstart examples, the Gin Web framework seemed easier to understand.

First of all, I will not be talking about setup and installation of Go. I am assuming that the reader has sufficient knowledge in setting up Go. I'm on Windows 10 using Visual Studio Code along with it's Go extensions to edit my code. I'd recommend running go install github.com/go-delve/delve/cmd/dlv@latest on the command line so that you can do "Run and Debug" in VS Code.

Baby Steps

  1. Create a go-chi-hello directory and go in.
  2. Execute go mod init go-chi-hello on the command line. This will create a go.mod file in the current directory.
  3. Create main.go. Copy the contents from below.
  4. Execute go mod tidy on the command line. This will update go.mod, adding the dependencies.
  5. Execute go run main.go on the command line. Hit the endpoint using a browser, Postman or whatever you like.

main.go


package main

import (
	"net/http"

	"github.com/go-chi/chi/middleware"
	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte("{ \"message\": \"Hello World!\" }"))
	})
	http.ListenAndServe(":3000", r)
}

Pretty simple program. An HTTP GET request triggers a response of "Hello World!" in JSON format. The server is listening on port 3000.

You should see something like below when a request comes if you have the logger set.


2022/10/04 15:27:43 "GET http://localhost:3000/ HTTP/1.1" from 127.0.0.1:56005 - 200 29B in 0s
2022/10/04 15:27:45 "GET http://localhost:3000/ HTTP/1.1" from 127.0.0.1:56005 - 200 29B in 0s

On Firefox developer, you should see something like below if the content type is set to JSON otherwise you'll see the other one.  

go-chi JSON Response


 
go-chi Plaintext Response

There you have it. The simplest endpoint to start with. Next step would be to hook this up with Accessing MySQL using Go Example.

Tuesday, July 5, 2022

Accessing MySQL using Go Example

In this example, I will be accessing a relational database (MySQL) using the Go programming language. I have followed the tutorial from the official Go website. Unfortunately, on my first try, I wasn't able to connect to my database. I'm writing this to supplement what was written on the Go website.

First of all, I will not be talking about setup and installation of MySQL and Go. I am assuming that the reader has sufficient knowledge in setting up MySQL and Go. I'm using Visual Studio Code along with it's Go extensions to edit my code.

MySQL setup

Create a database called recordings. In the recordings schema, create the table and values as seen below. Would be easier if you have MySQL Workbench but you can also go command line or however you like. Don't forget to use recordings.


DROP TABLE IF EXISTS album;
CREATE TABLE album (
  id         INT AUTO_INCREMENT NOT NULL,
  title      VARCHAR(128) NOT NULL,
  artist     VARCHAR(255) NOT NULL,
  price      DECIMAL(5,2) NOT NULL,
  PRIMARY KEY (`id`)
);

INSERT INTO album
  (title, artist, price)
VALUES
  ('Blue Train', 'John Coltrane', 56.99),
  ('Giant Steps', 'John Coltrane', 63.99),
  ('Jeru', 'Gerry Mulligan', 17.99),
  ('Sarah Vaughan', 'Sarah Vaughan', 34.98);

Next is to create your username and password. I have created "golang" as my username and password in MySQL.

main.go


package main

import (
	"database/sql"
	"fmt"
	"log"

	"github.com/go-sql-driver/mysql"
)

var db *sql.DB

func main() {
	// Capture connection properties.
	cfg := mysql.Config{
		User:   os.Getenv("DBUSER"),
		Passwd: os.Getenv("DBPASS"),
		Net:    "tcp",
		Addr:                 "127.0.0.1:3306",
		DBName:               "recordings",
		AllowNativePasswords: true, // if not included, mysql native password authentication error is generated
	}
	// Get a database handle.
	var err error
	db, err = sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		log.Fatal(err)
	}

	pingErr := db.Ping()
	if pingErr != nil {
		log.Fatal(pingErr)
	}
	fmt.Println("Connected!")
}

The above code is the same as the one in the Go Dev Tutorial. Except for one line, AllowNativePasswords: true. This line was missing in the Go Dev Tutorial and caused an error on my first go run .. This led me to some googling around. I found interesting stuff and I've collated them here. Hopefully it will help others who are starting to learn the Go languange. By the way, this should show Connected!.

Environment Variables

I'm on a Windows 10 machine so to set an environment variable on the command line, do set DBUSER=golang. If you don't want to use the command line because you are using Visual Studio Code. You'd want to run the code on the VSCode terminal, do $env:DBUSER = "golang". That's how to set an environment variable in powershell.

Addr

So I've tried this code using two MySQL databases. On MySQL 5.7.33, Addr: "127.0.0.1:3306" works fine. On MySQL 5.6.43, Addr: "localhost:3306" works fine. But if you swap the hostnames, you get and access denied for user error.

AllowNativePasswords

If you don't want to use AllowNativePasswords then you could replace cfg := mysql.Config() with cfg := mysql.NewConfig(). Something like below:


cfg := mysql.NewConfig()
cfg.User = os.Getenv("DBUSER")
cfg.Passwd = os.Getenv("DBPASS")
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "recordings"

There you have it. Now you don't have to do too much googling around. The complete project can be cloned from github.com/jpllosa/go-relational-database.