Sunday, April 14, 2024

GitLab to GitHub Migration Example

In this example, I'll demonstrate one way of migrating a repository from GitLab to GitHub. Sounds simple enough? Kinda? Here are the requirements and a caveat.

  • Preserve as much history as possible (e.g. tags, branches, commits, merges, etc.).
  • The GitLab repo is on premise and is only accessible through the organization VPN.

Now this would have been easy if your GitLab repo can be seen by GitHub. Sadly it can't because your repo is behind a firewall. What do we do now? Well the good thing is both are git repositories. I did these steps on a Windows 11 machine with git for Windows v2.44.

  1. Create a new repo on GitHub. Make sure to name it the same as the one on GitLab.

  2. On git bash, make a "bare" clone of the GL (GitLab) repo in your local machine. This creates a full copy of the data, but without a working directory for editing files, and ensures a clean, fresh export of all the old data.

    
    	# $ git clone --bare https://gitlab-host.com/username/repo.git
    	
    	$ git clone --bare https://gitlab.com/jpllosa/migration-demo.git
    	Cloning into bare repository 'migration-demo.git'...
    	remote: Enumerating objects: 11, done.
    	remote: Counting objects: 100% (11/11), done.
    	remote: Compressing objects: 100% (8/8), done.
    	remote: Total 11 (delta 3), reused 0 (delta 0), pack-reused 0
    	Receiving objects: 100% (11/11), done.
    	Resolving deltas: 100% (3/3), done.
        
    	
  3. Push the locally cloned repository to GH (GitHub) using the "mirror" option. This ensures that all references, such as branches and tags, are copied to the imported repository.

    
    	# $ cd repo.git
    	# $ git push --mirror https://github.com/username/repo.git
    
    	$ cd migration-demo.git/
    	user@hostname MINGW64 /c/temp/migration-demo.git (BARE:main)
    	$ git push --mirror https://github.com/jpllosa/migration-demo.git
    	Enumerating objects: 11, done.
    	Counting objects: 100% (11/11), done.
    	Delta compression using up to 20 threads
    	Compressing objects: 100% (5/5), done.
    	Writing objects: 100% (11/11), 3.57 KiB | 3.57 MiB/s, done.
    	Total 11 (delta 3), reused 11 (delta 3), pack-reused 0 (from 0)
    	remote: Resolving deltas: 100% (3/3), done.
    	To https://github.com/jpllosa/migration-demo.git
    	 * [new branch]      branch-two -> branch-two
    	 * [new branch]      main -> main
    	 * [new tag]         v1.0 -> v1.0
        
    	

    As you can see from above, it looks like everything got copied over. Branches, tags and all that jazz. A thing to note though, if you got various git accounts from different git servers then you might need to change some configuration before you can push. See git config command for more details.

  4. All done. You can now remove the temporary local repo and then use the GH repo from now on.

    
        cd ..
        rm -rf REPO.git
        
    	

GitLab to GitHub Migration

Were the branches preserve? As can bee seen below, it has been.

Were the tags preserve? As can bee seen below, it has been.

Were the commits preserve? As can bee seen below, it has been.

Awesome! That was a successful migration. Great job!

Some Reminders

  • On GH don't forget to configure the default branch settings, add collaborators and teams, etc.
  • If your repo is a Maven project, don't forget to update the scm details.
  • Don't forget to update your CI/CD pipelines. For example update your Jenkins job to pull from the new GH repo.

GitLab to GitHub Migration Closing

Now you know how to migrate your git repo from GL to GH. Everything got preserved as well even when your GL server was behind a firewall.

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, January 1, 2024

XML to Java Class Example

Have you got a task to convert XML data to Java objects? Worry no more. Java 1.8 has got you covered.

For this example, we are going to use a subset of XML data from dictionary of medicines and devices. Making this as close as possible to real world use. The XML Schema Definition describes the structure of an XML document. The XML document is the data we are going to convert into Java objects. The files are:

  • f_vtm2_3301123.xml
  • vtm_v2_3.xsd

New Java Project

I'm using Spring Tool Suite 4 and Java 1.8 for this project. You can use whatever IDE you like. Create a new project like so:

XSD to Java Types

I've placed the above mentioned XML and XSD files under the data folder. I'm on Windows 10 so on the Command Prompt I go to the data folder. I have JDK configured on my PATH which makes me do the xjc command. For more information, run xjc -help. Below I have specified the -p option which specifies the target package.


D:\xml-to-java\data>xjc -p com.blogspot.jpllosa vtm_v2_3.xsd
parsing a schema...
compiling a schema...
com\blogspot\jpllosa\ObjectFactory.java
com\blogspot\jpllosa\VIRTUALTHERAPEUTICMOIETIES.java

A million thanks JDK for the xjc command. We now have Java classes created. Let's move the package to our src folder.

XML to Java

Let's create Main.java to drive the XML to Java object conversion. You should have something like below:


package com.blogspot.jpllosa;

import java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

public class Main {
    public static void main(String[] args) throws JAXBException {
        System.out.println("Starting...");
        File file = new File("data/f_vtm2_3301123.xml");
        JAXBContext jcVtm = JAXBContext.newInstance(VIRTUALTHERAPEUTICMOIETIES.class);
        Unmarshaller unmarshaller = jcVtm.createUnmarshaller();
        VIRTUALTHERAPEUTICMOIETIES vtms = (VIRTUALTHERAPEUTICMOIETIES) unmarshaller.unmarshal(file);
        
        int size = vtms.getVTM().size();
        if (size > 0) {
            System.out.printf("Virtual Therapeutic Moieties: %,d \r\n", size);
            for (int i = 0; i < 5; i++) {
                System.out.println(vtms.getVTM().get(i).getNM());
            }
        }
    }
}

Pretty cool. In a few minutes we are able to read the XML document and map it to Java objects. As Java objects, we can now do whatever we like with it. The super simple steps are open the file, create a JAXB context then the unmarshaller does all the heavy lifting. For this example we just show the first 5 VTMs out of the 3,000 or so Virtual Therapeutic Moieties. Right click on the file then Run As Java Application. The output looks like below:


Starting...
Virtual Therapeutic Moieties: 3,117 
Acebutolol
Paracetamol
Acetazolamide
Abciximab
Acarbose

There you have it. A super quick example of converting XML data to Java objects. The complete project can be cloned from github.com/jpllosa/xml-to-java.