goshopify

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2025 License: MIT Imports: 8 Imported by: 0

README

About

Go-Shopify is a SDK to interact with the Shopify API.

Note: This is a work-in-progress. As of 29/03/2025: The GraphQL API for a couple of modules, is working. Read the Changelog for more.


Workflows

Usage with a Shopify Embedded App

func main() {
    // Base Config for application
    cfg := goshopify.Config{
        ClientID: "shopify-app-client-id",
        ClientSecret: "shopify-app-client-secret",
        ApiVersion: "2025-01",
        Scopes: []string{"read_orders", "read_customers", "read_products"},
        AccessMode: "offline",
        RedirectURL: "",
        Timeout: 10 * time.Second,
    }

    // Create a Shopify Client
    client := goshopify.NewShopifyClient(&cfg)

    // In a Embedded Shopify App, the session bearer token must be exchanged for an access token (called the token exchange flow)
    // goshopify comes with middleware helpers to validate and exchange session token for an access token.
}

For the implementation of the middleware, the following example is using the Echo framework for Go, but can easily be adjusted for any other framework.

In common.go,

// We need to pass the Shopify client to the request handlers.
// For that we will define a ShopifyContext type, that will contain the information the handlers need to make Shopify API calls from within handlers.

import (
    "github.com/labstack/echo/v4"
)

type ShopifyContext struct {
    echo.Context
    Shopify *ShopifyClient
    // ...Any other information you want to pass to handlers
}

// Constructor that returns a new ShopifyContext
func NewShopifyContext(c echo.Context, client *ShopifyClient) *ShopifyContext {
    ctx := new(ShopifyContext)
    ctx.Context = c
    ctx.Shopify = client

    return ctx
}

In middleware.go,

// As the shopify app (frontend) sends a request to the backend, there should be middleware to complete the token flow and get an access token.
// For shopify apps with the `offline` access scope, you should securely store this access token once you receive it, as it will not change for the store,
// unless the app is uninstalled and reinstalled.

import (
    "net/http"

    "github.com/zenius-one/go-shopify/config"
    "github.com/zenius-one/go-shopify/session"
)

/********************************************************************************************************************/
// ShopifyContextMiddleware
/********************************************************************************************************************/

// Takes a config struct and returns a Middleware with a `ShopifyContext` context which aggregates `echo.Context`.
// This middleware must run before the others
func ShopifyContextMiddleware(cfg *config.Config) func(next echo.HandlerFunc) echo.HandlerFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// Create an instance of your context struct.
			// This way, every request gets it's own instance, allowing concurrent operations without race conditions.
			ctx := NewShopifyContext(c, &config{
                ClientID: cfg.ClientID,
                ClientSecret: cfg.ClientSecret,
                ApiVersion: cfg.ApiVersion,
                Scopes: cfg.Scopes,
                AccessMode: cfg.AccessMode,
                RedirectURL: cfg.RedirectURL,
                Timeout: cfg.Timeout,
            })

			// Pass the custom context to the next handler
			return next(ctx)
		}
	}
}

/********************************************************************************************************************/
// ShopifyValidateAndExchangeSessionToken (Using Echo framework for Go)
/********************************************************************************************************************/
// Validates Shopify Session for API requests.
// Reference: https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-app-express/src/middlewares/validate-authenticated-session.ts
func ShopifyValidateAndExchangeSessionToken(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
		sessionToken := c.Request().Header.Get("Authorization")

        // Context typecasting for quick access to the additional context defined earlier
		ctx, ok := c.(*ShopifyContext)
		if !ok {
			return fmt.Errorf("ShopifyValidateAndExchangeSessionToken middleware not typecast echo.Context into *ShopifyContext")
		}

        // Validate session
        err := session.ValidateSession(ctx.Shopify, sessionToken)
        if err != nil {
            fmt.Println("Session token validation error:", err.Error())
            return c.String(http.StatusBadRequest, `{"message": "Session token validation failed"}`)
        }

        // Exchange token
        accessToken, err := session.ExchangeToken(ctx.Shopify, sessionToken, &session.TokenExchangeOpts{
            CacheGet: ctx.GetCache,
            CacheSet: ctx.SetCache
        })
        if err != nil {
            fmt.Println("Token exchange failed:", err.Error())
            return c.String(http.StatusBadRequest, `{"message": "Token exchange failed"}`)
        }

        // Set accessToken to Shopify client
        ctx.Shopify.SetAccessToken(accessToken)

        return next(ctx)
    }
}

In routes.go,

// Define routes to access below handler here.

In a handler function, say shopify.products.go,

// Get Products
func GetProducts(c echo.Context) error {
	// Context typecasting for quick access to additional context
	ctx, ok := c.(*ShopifyContext)
	if !ok {
		return fmt.Errorf("GetProducts could not typecast echo.Context into *handlers.ShopifyContext")
	}

    // Get All Products
    productConn, err := ctx.Shopify.GraphQL.Admin.Products.List(c.Request().Context(), "", true)
    if err != nil {
        fmt.Println("Token exchange failed:", err.Error())
        return c.String(http.StatusBadRequest, `{"message": "Error listing all products"}`)
    }

	return c.JSONPretty(http.StatusOK, productConn.Nodes, "  ")
}

Authorization Code Flow

func main() {
    cfg := goshopify.Config{
        ShopName: "quickstart",
        ClientID: "shopify-app-client-id",
        ClientSecret: "shopify-app-client-secret",
        ApiVersion: "2025-01",
        Scopes: []string{"read_orders", "read_customers", "read_products"},
        AccessMode: "offline",
        RedirectURL: "",
        Timeout: 10 * time.Second,
    }

    client := goshopify.NewClient()

    // Create nonce
    state := shortid.MustGenerate()

    // Start Authorization code Flow
    authCodeResp := client.Authorize(state)

    // Start Token Flow
    tokenResp, err := client.GetAccessToken(authCodeResp.Code)
    if err != nil {
        return err
    }

    // Make Authenticated Requests
    products := client.GraphQL.Admin.All.GetProducts()
}


Directly creating a Shopify GraphQL API Client

You can make Shopify API calls if you have:

  • A Shop URL,
  • Shopify API Version,
  • Access Token

Embedded apps can fetch the Shopify Access Token via Token Exchange flow, bypassing the need to do an authorization code flow. We can use this information then, and simply create an API Client Directly

func main() {
    client := goshopify.NewShopifyGraphQLClientWithDefaultHeaders(os.Getenv("SHOPIFY_SHOP_URL"), os.Getenv("SHOPIFY_API_VERSION"), os.Getenv("SHOPIFY_ACCESS_TOKEN"), nil)

    // Make API calls
    c := context.Background()

    // Get Orders Count
    count, err := api.Admin.Order.Count(c)
    if err != nil {
        log.Println("ERROR: Failed to get orders count")
    }

    // List first 250 Customers
    customersConnection, err := api.Admin.Customer.List(c, "")
    if err != nil {
		log.Println("ERROR: Failed to list all customers")
	}

    // List all Products
    products, err := api.Admin.Product.ListAll(c, "")
    if err != nil {
		log.Println("ERROR: Failed to list all products")
	}
}

Documentation

Index

Constants

View Source
const (
	CUSTOMERS_PER_PAGE int = 250
)
View Source
const DEFAULT_CUSTOMERS_COUNT_QUERY = `query {
	customersCount(query: "id:>=1") {
	  count
  }
}`
View Source
const DEFAULT_LOCATIONS_COUNT_QUERY = `query {
	locationsCount(query: "id:>=1") {
	  count
  }
}`
View Source
const DEFAULT_ORDERS_COUNT_QUERY = `query {
	ordersCount(query: "id:>=1") {
	  count
  }
}`
View Source
const DEFAULT_PRODUCTS_COUNT_QUERY = `query {
  	productsCount(query: "id:>=1") {
		count
	}
}`
View Source
const (
	LOCATIONS_PER_PAGE = 250
)
View Source
const (
	ORDERS_PER_PAGE = 250
)
View Source
const (
	PRODUCTS_PER_PAGE int = 100
)
View Source
const (
	SHOPIFY_CURRENT_API_VERSION = `2025-01`
)

Variables

View Source
var DEFAULT_CUSTOMERS_QUERY = `query ListCustomers($numCustomers: Int!, $cursor: String) {
	customers(first: $numCustomers, after: $cursor) {
		nodes {
			id
			legacyResourceId
			firstName
			lastName
			displayName
			image {
				...ImageFields
			}
			email
			emailMarketingConsent {
				marketingState
				marketingOptInLevel
				consentUpdatedAt
			}
			smsMarketingConsent {
				marketingState
				marketingOptInLevel
				consentUpdatedAt
				consentCollectedFrom
			}
			verifiedEmail
			dataSaleOptOut
			phone
			state
			addressesV2(first: 10) {
				nodes {
					...MailingAddressFields
				}
			}
			defaultAddress {
				...MailingAddressFields
			}
			locale
			lastOrder {
				id
				name
			}
			productSubscriberStatus
			metafields(first: 20) {
				nodes {
					id
					key
					namespace
					jsonValue
				}
			}
			multipassIdentifier
			numberOfOrders
			unsubscribeUrl
			taxExempt
			taxExemptions
			statistics {
				predictedSpendTier
			}
			amountSpent {
				amount
				currencyCode
			}
			note
			tags
			createdAt
			updatedAt
		}
		pageInfo {
			hasNextPage
		  	endCursor
		}
	}
}` + mailingAddressFragment + imageFragment

ListCustomers

View Source
var DEFAULT_LOCATIONS_QUERY string = `` /* 475-byte string literal not displayed */

List Locations

View Source
var DEFAULT_ORDERS_QUERY = `query ListOrders($numOrders: Int!, $cursor: String) {
	orders(first: $numOrders, after: $cursor) {
		nodes {
			id
			legacyResourceId
			name
			poNumber
			email
			phone
			app {
				id
				name
			}
			clientIp
			customerAcceptsMarketing
			cancelReason
			cancelledAt
			confirmed
			confirmationNumber
			closed
			closedAt
			customer {
				id
				legacyResourceId
				firstName
				lastName
				state
				phone
				email
				verifiedEmail
				emailMarketingConsent {
					marketingState
				}
				smsMarketingConsent {
					marketingState
				}
				multipassIdentifier
				taxExempt
				taxExemptions
				note
				tags
				createdAt
				updatedAt
			}
			customerLocale
			customerJourneySummary {
				customerOrderIndex
				daysToConversion
				lastVisit {
					landingPage
					source
					sourceType
					referrerUrl
					referralCode
					marketingEvent {
						id
						legacyResourceId
						app {
							id
						}
						remoteId
						channelHandle
						type
						marketingChannelType
						sourceAndMedium
						previewUrl
						manageUrl
						scheduledToEndAt
						endedAt
					}
					occurredAt
				}
			}
			billingAddress {
				...MailingAddressFields
			}
			shippingAddress {
				...MailingAddressFields
			}
			currencyCode
			presentmentCurrencyCode
			displayFinancialStatus
			displayFulfillmentStatus
			statusPageUrl
			currentTotalAdditionalFeesSet {
				...MoneyBagFields
			}
			currentTotalDiscountsSet {
				...MoneyBagFields
			}
			currentTotalDutiesSet {
				...MoneyBagFields
			}
			currentTotalPriceSet {
				...MoneyBagFields
			}
			currentSubtotalPriceSet {
				...MoneyBagFields
			}
			currentTotalTaxSet {
				...MoneyBagFields
			}
			discountApplications {
				nodes {
					index
					targetType
					value {
						... on MoneyV2 {
							amount
							currencyCode
						}
						... on PricingPercentageValue {
							percentage
						}
					}
				}
			}
			discountCodes
			estimatedTaxes
			fulfillable
			fulfillments {
				id
				legacyResourceId
				name
				status
				order {
					id
				}
				fulfillmentLineItems {
					nodes {
						id
						lineItem {
							...LineItemFields
						}
						quantity
					}
				}
				trackingInfo {
					company
					number
					url
				}
				createdAt
				estimatedDeliveryAt
				inTransitAt
				deliveredAt
				updatedAt
			}
			lineItems {
				nodes {
					...LineItemFields
				}
			}
			originalTotalDutiesSet {
				...MoneyBagFields
			}
			paymentTerms {
				id
				overdue
				dueInDays
				order {
					totalOutstandingSet {
						...MoneyBagFields
					}
				}
				paymentTermsName
				paymentTermsType
				paymentSchedules {
					nodes {
						id
						issuedAt
						dueAt
						completedAt
					}
				}
			}
			paymentGatewayNames
			fullyPaid
			refunds {
				id
				legacyResourceId
				order {
					id
				}
				orderAdjustments {
					nodes {
						id
						reason
						amountSet {
							...MoneyBagFields
						}
						taxAmountSet {
							...MoneyBagFields
						}
					}
				}
				refundLineItems {
					nodes {
						id
						lineItem {
							...LineItemFields
						}
						priceSet {
							...MoneyBagFields
						}
						subtotalSet {
							...MoneyBagFields
						}
						totalTaxSet {
							...MoneyBagFields
						}
						quantity
						restocked
						restockType
						location {
							id
							legacyResourceId
						}
					}
				}
				totalRefundedSet {
					...MoneyBagFields
				}
				note
				staffMember {
					id
					name
					isShopOwner
					active
				}
				createdAt
				updatedAt
			}
			subtotalPriceSet {
				...MoneyBagFields
			}
			staffMember {
				id
				name
				isShopOwner
				active
			}
			test
			taxesIncluded
			taxExempt
			taxLines {
				title
				priceSet {
					...MoneyBagFields
				}
				rate
				ratePercentage
				source
				channelLiable

			}
			totalCashRoundingAdjustment {
				paymentSet {
					...MoneyBagFields
				}
				refundSet {
					...MoneyBagFields
				}
			}
			totalDiscountsSet {
				...MoneyBagFields
			}
			totalOutstandingSet {
				...MoneyBagFields
			}
			totalPriceSet {
				...MoneyBagFields
			}
			totalShippingPriceSet {
				...MoneyBagFields
			}
			totalTaxSet {
				...MoneyBagFields
			}
			totalTipReceivedSet {
				...MoneyBagFields
			}
			totalWeight
			note
			tags
			createdAt
			processedAt
			updatedAt
		}
		pageInfo {
			hasNextPage
		  	endCursor
		}
	}
}` + moneyBagFragment + lineItemFragment + mailingAddressFragment

****************************************************************************************************************** QUERIES ******************************************************************************************************************

View Source
var DEFAULT_PRODUCTS_QUERY string = `` /* 1706-byte string literal not displayed */
View Source
var DEFAULT_SHOP_LOCALES_QUERY = `query GetShopLocales() {
	shopLocales {
		name
		locale
		primary
		published
	}
}`
View Source
var DEFAULT_SHOP_QUERY string = `` /* 983-byte string literal not displayed */

Functions

This section is empty.

Types

type CustomerService

type CustomerService interface {
	List(c context.Context, cursor string) (*types.CustomerConnection, error)
	ListAll(c context.Context, cursor string) (*types.CustomerConnection, error)
	Count(c context.Context) (int, error)
}

type CustomerServiceOp

type CustomerServiceOp struct {
	// contains filtered or unexported fields
}

CustomerServiceOp handles communication with the product related methods of the Shopify API.

func (*CustomerServiceOp) Count

func (s *CustomerServiceOp) Count(c context.Context) (int, error)

Return orders count

func (*CustomerServiceOp) List

func (*CustomerServiceOp) ListAll

type GetCustomersCountResponse

type GetCustomersCountResponse struct {
	CustomersCount *types.Count `json:"customersCount,omitempty"`
}

type GetCustomersResponse

type GetCustomersResponse struct {
	Customers *types.CustomerConnection `json:"customers"`
}

type GetLocationsCountResponse

type GetLocationsCountResponse struct {
	LocationsCount *types.Count `json:"locationsCount,omitempty"`
}

type GetLocationsResponse

type GetLocationsResponse struct {
	Locations *types.LocationConnection `json:"locations"`
}

type GetOrdersCountResponse

type GetOrdersCountResponse struct {
	OrdersCount *types.Count `json:"ordersCount,omitempty"`
}

type GetOrdersResponse

type GetOrdersResponse struct {
	Orders *types.OrderConnection `json:"orders"`
}

type GetProductsCountResponse

type GetProductsCountResponse struct {
	ProductsCount *types.Count `json:"productsCount,omitempty"`
}

type GetProductsResponse

type GetProductsResponse struct {
	Products *types.ProductConnection `json:"products,omitempty"`
}

type GetShopLocalesResponse

type GetShopLocalesResponse struct {
	ShopLocales []types.ShopLocale `json:"shopLocales"`
}

type GetShopResponse

type GetShopResponse struct {
	Shop *types.Shop `json:"shop,omitempty"`
}

type GraphQLAPI

type GraphQLAPI struct {
	Admin      *GraphQLAdmin
	Storefront *GraphQLStorefront
	// contains filtered or unexported fields
}

GraphQL API struct that exposes the Admin and the Storefront API

func NewShopifyGraphQLClientWithDefaultHeaders

func NewShopifyGraphQLClientWithDefaultHeaders(shopURL, apiVersion, accessToken string, defaultHeaders map[string]string) *GraphQLAPI

New Shopify GraphQL Client constructor

type GraphQLAdmin

type GraphQLAdmin struct {

	// Services used for communicating with the API
	Customer CustomerService
	Location LocationService
	Order    OrderService
	Product  ProductService
	Shop     ShopService
	// contains filtered or unexported fields
}

GraphQL Admin Client

func (*GraphQLAdmin) Query

func (admin *GraphQLAdmin) Query(c context.Context, query string, vars map[string]any, resp any) error

Query creates a graphql query against the Shopify Admin API The "data" portion of the response is unmarshalled into resp

type GraphQLAdminClient

type GraphQLAdminClient interface {
	Query(c context.Context, query string, vars map[string]interface{}, resp interface{}) error
}

A GraphQL Client with methods to interact with the Shopify GraphQL Admin API

type GraphQLStorefront

type GraphQLStorefront struct {
	// contains filtered or unexported fields
}

GraphQL Storefront Client

func (*GraphQLStorefront) Query

func (sf *GraphQLStorefront) Query(c context.Context, query string, vars map[string]any, resp any) error

Query creates a graphql query against the Shopify Storefront API The "data" portion of the response is unmarshalled into resp

type GraphQLStorefrontClient

type GraphQLStorefrontClient interface {
	Query(c context.Context, query string, vars map[string]interface{}, resp interface{}) error
}

A GraphQL Client with methods to interact with the Shopify GraphQL Storefront API

type LocationService

type LocationService interface {
	List(c context.Context, cursor string) (*types.LocationConnection, error)
	ListAll(c context.Context, cursor string) (*types.LocationConnection, error)
	Count(c context.Context) (int, error)
}

LocationService is an interface for interfacing with the location endpoints of the Shopify API.

type LocationServiceOp

type LocationServiceOp struct {
	// contains filtered or unexported fields
}

LocationServiceOp handles communication with the location related methods of the Shopify API.

func (*LocationServiceOp) Count

func (s *LocationServiceOp) Count(c context.Context) (int, error)

Return lcoations count

func (*LocationServiceOp) List

Lists locations

func (*LocationServiceOp) ListAll

Lists all locations, iterating over pages

type OrderService

type OrderService interface {
	List(c context.Context, cursor string) (*types.OrderConnection, error)
	ListAll(c context.Context, cursor string) (*types.OrderConnection, error)
	Count(c context.Context) (int, error)
}

type OrderServiceOp

type OrderServiceOp struct {
	// contains filtered or unexported fields
}

OrderServiceOp handles communication with the order / orders related methods of the Shopify GraphQL API.

func (*OrderServiceOp) Count

func (s *OrderServiceOp) Count(c context.Context) (int, error)

Return orders count

func (*OrderServiceOp) List

Lists orders

func (*OrderServiceOp) ListAll

func (s *OrderServiceOp) ListAll(c context.Context, cursor string) (*types.OrderConnection, error)

Lists all orders, iterating over pages

type ProductService

type ProductService interface {
	List(c context.Context, cursor string) (*types.ProductConnection, error)
	ListAll(c context.Context, cursor string) (*types.ProductConnection, error)
	Count(c context.Context) (int, error)
}

type ProductServiceOp

type ProductServiceOp struct {
	// contains filtered or unexported fields
}

ProductServiceOp handles communication with the product related methods of the Shopify API.

func (*ProductServiceOp) Count

func (s *ProductServiceOp) Count(c context.Context) (int, error)

Return products count

func (*ProductServiceOp) List

Lists products

func (*ProductServiceOp) ListAll

Lists all products, iterating over pages

type ShopService

type ShopService interface {
	Get(c context.Context) (*types.Shop, error)
	ListLocales(c context.Context) ([]types.ShopLocale, error)
}

type ShopServiceOp

type ShopServiceOp struct {
	// contains filtered or unexported fields
}

ShopServiceOp handles communication with the shop related methods of the Shopify GraphQL API.

func (*ShopServiceOp) Get

func (s *ShopServiceOp) Get(c context.Context) (*types.Shop, error)

Get shop information

func (*ShopServiceOp) ListLocales

func (s *ShopServiceOp) ListLocales(c context.Context) ([]types.ShopLocale, error)

Get a list of locales available on a shop.

type ShopifyClient

type ShopifyClient struct {
	GraphQL *GraphQLAPI // The GraphQL API
	// contains filtered or unexported fields
}

func NewShopifyClient

func NewShopifyClient(cfg *config.Config) (*ShopifyClient, error)

Create new Shopify Client

func (*ShopifyClient) AccessMode

func (c *ShopifyClient) AccessMode() string

Get Read-only Access Mode

func (*ShopifyClient) AccessToken

func (c *ShopifyClient) AccessToken() string

Get Read-only Access Token

func (*ShopifyClient) ClientId

func (c *ShopifyClient) ClientId() string

Get Read-only Shopify Client ID

func (*ShopifyClient) ClientSecret

func (c *ShopifyClient) ClientSecret() string

Get Read-only Shopify Client Secret

func (*ShopifyClient) GetConfig

func (c *ShopifyClient) GetConfig() *config.Config

Get Read-only config. Use Setter functions to update Config after initialization.

func (*ShopifyClient) InitDefaultGraphQLClient

func (c *ShopifyClient) InitDefaultGraphQLClient() error

Initialize GraphQL Client

func (*ShopifyClient) IsReady

func (c *ShopifyClient) IsReady() (bool, error)

Whether client is ready to serve API requests

func (*ShopifyClient) RedirectURL

func (c *ShopifyClient) RedirectURL() string

Get Read-only Redirect URL

func (*ShopifyClient) Scopes

func (c *ShopifyClient) Scopes() []string

Get Read-only Scopes

func (*ShopifyClient) SetAccessToken

func (c *ShopifyClient) SetAccessToken(token string)

Set the Access Token manually.

Example use case: When using an Shopify Embedded App flow, where you get an access token via Token Exchange

func (*ShopifyClient) SetShopDomain

func (c *ShopifyClient) SetShopDomain(domain string)

Set shop domain in the format `{shop}.myshopify.com`

func (*ShopifyClient) SetShopURL

func (c *ShopifyClient) SetShopURL(s string)

Set shop url in the format `https://{shop}.myshopify.com`

func (*ShopifyClient) ShopDomain

func (c *ShopifyClient) ShopDomain() string

Get Read-only Shop Domain in the format: `{shop}.myshopify.com`

func (*ShopifyClient) ShopURL

func (c *ShopifyClient) ShopURL() string

Get Read-only Shop URL in the format: `https://{shop}.myshopify.com`

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL