able to pull Products using Shopify GraphQL API

This commit is contained in:
Ben Miller
2024-11-09 02:40:13 -07:00
parent 6bbd8d4525
commit fb86c9c96d
4 changed files with 786 additions and 4 deletions

View File

@ -5,6 +5,8 @@
} }
], ],
"settings": { "settings": {
"prettier.semi": false "prettier.semi": false,
"cloudcode.duetAI.project": "beepmill-code",
"cloudcode.project": "beepmill-code"
} }
} }

View File

@ -1,10 +1,43 @@
class Config { export class Config {
productPhotosFolderId: string productPhotosFolderId: string
shopifyApiKey: string
shopifyApiSecretKey: string
shopifyAdminApiAccessToken: string
shopifyApiURI: string
constructor() { constructor() {
let ss = SpreadsheetApp.getActive() let ss = SpreadsheetApp.getActive()
let s = ss.getSheetByName("vars") let s = ss.getSheetByName("vars")
this.productPhotosFolderId = vlookupByColumns("vars", "key", "productPhotosFolderId", "value") this.productPhotosFolderId = vlookupByColumns(
"vars",
"key",
"productPhotosFolderId",
"value"
)
this.shopifyApiKey = vlookupByColumns(
"vars",
"key",
"shopifyApiKey",
"value"
)
this.shopifyApiSecretKey = vlookupByColumns(
"vars",
"key",
"shopifyApiSecretKey",
"value"
)
this.shopifyAdminApiAccessToken = vlookupByColumns(
"vars",
"key",
"shopifyAdminApiAccessToken",
"value"
)
this.shopifyApiURI = vlookupByColumns(
"vars",
"key",
"shopifyApiURI",
"value"
)
} }
} }

View File

@ -1,7 +1,22 @@
import { Shop } from "./shopifyApi"
function initMenu() { function initMenu() {
let ui = SpreadsheetApp.getUi() let ui = SpreadsheetApp.getUi()
ui.createMenu("BLM") ui.createMenu("BLM")
.addItem("Fill out product from template", "fillProductFromTemplate") .addItem("Fill out product from template", "fillProductFromTemplate")
.addItem("Create missing photo folders", "createMissingPhotoFolders") .addItem("Create missing photo folders", "createMissingPhotoFolders")
.addSeparator()
.addItem("Run Shopify Orders", "runShopifyOrders")
.addItem("Get Shopify Products", "getShopifyProducts")
.addToUi() .addToUi()
} }
function runShopifyOrders() {
let shop = new Shop()
shop.RunOrders()
}
function getShopifyProducts() {
let shop = new Shop()
shop.GetProducts()
}

732
src/shopifyApi.ts Normal file
View File

@ -0,0 +1,732 @@
// EXTREMELY inspired by https://github.com/webdjoe/shopify_apps_script/blob/master/shopify_api.gs
// Create a private App in Shopify admin and insert app password below
// Sheets need to be created before running with columns of desired output set
// Use keys found in Shopify Rest API Order Object as row headers
// https://shopify.dev/api/admin-rest/2022-04/resources/order#resource-object
// Supports the following sheets/order objects with default columns -
// orders - id, number, order_name, name, current_total_discounts, current_total_price, current_subtotal_price, current_total_tax, financial_status, landing_site, created_at, processed_at, processing_method, subtotal_price, source_url, tags, token, total_discounts, total_line_items_price, total_price, total_tax, updated_at
// shipping_lines - order_id, ordered_at, code, price, title, source, discounted_price, carrier_identifier
// discount_applications - order_id, created_at, type, title, value, value_type, code, description, target_type, target_selection, allocation_method, ordered_at
// refunds = id, order_id, ordered_at, created_at, note, transaction_id, kind, amount
// line_items = id, order_id, sku, name, price, title, vendor, quantity, product_id, variant_id, variant_title, total_discount, fulfillment_status, ordered_at
// customer - order_id, ordered_at, id, email, phone, last_name, created_at, first_name, admin_graphql_api_id, updated_at, orders_count, last_order_id
/// <reference types="@types/google-apps-script" />
import { Config } from "./config"
const ss = SpreadsheetApp.getActive()
const today = new Date()
const thirty_days = new Date(
today.getFullYear(),
today.getMonth() - 1,
today.getDate()
)
const start_str = Utilities.formatDate(
thirty_days,
SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(),
"yyyy-MM-dd"
)
const end_str = Utilities.formatDate(
today,
SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(),
"yyyy-MM-dd"
)
const orders = "_orders"
const line_items = "_line_items"
const customer = "_customer"
const refunds = "_refunds"
const shipping_lines = "_shipping_lines"
const discount_applications = "_discount_applications"
function makeArray(headers, obj_list) {
var rng_arr = []
for (let obj of obj_list) {
var line_arr = []
headers.forEach((item) => {
var line_item = item in obj ? obj[item] : ""
if (Array.isArray(line_item)) {
line_item = line_item.toString()
}
line_arr.push(line_item)
})
rng_arr.push(line_arr)
}
return rng_arr
}
function makeDiscountsArray(orders_arr) {
var disc_arr = []
var headers = getDiscountHeaders()
for (let ord of orders_arr) {
var disc_list = ord.discount_applications
if (disc_list.length < 1) {
continue
}
disc_list.forEach((disc) => {
var disc_line = []
disc["order_id"] = ord["id"]
disc["ordered_at"] = ord["created_at"]
headers.forEach((head) => {
var itm = head in disc ? disc[head] : ""
disc_line.push(itm)
})
disc_arr.push(disc_line)
})
}
return disc_arr
}
function makeCustomerArray(orders_arr) {
var cust_arr = []
var headers = getCustomerHeaders()
for (let ord of orders_arr) {
var cust_obj = ord.customer
if (typeof cust_obj == "undefined") {
continue
}
var customer_line = []
cust_obj["order_id"] = ord["id"]
cust_obj["ordered_at"] = ord["created_at"]
headers.forEach((head) => {
var itm = head in cust_obj ? cust_obj[head] : ""
customer_line.push(itm)
})
cust_arr.push(customer_line)
}
return cust_arr
}
function makeLIArray(orders_arr) {
var line_arr = []
var headers = getLineItemHeaders()
for (let ord of orders_arr) {
var line_items_list = ord.line_items
if (line_items_list.length < 1) {
continue
}
line_items_list.forEach((li_obj) => {
var li_line = []
li_obj["ordered_at"] = ord["created_at"]
li_obj["order_id"] = ord["id"]
headers.forEach((head) => {
var itm = head in li_obj ? li_obj[head] : ""
li_line.push(itm)
})
line_arr.push(li_line)
})
}
return line_arr
}
function makeShippingArray(orders_arr) {
var ship_arr = []
var header = getShippingHeaders()
for (let ord of orders_arr) {
var ship_list = ord.shipping_lines
if (ship_list.length < 1) {
continue
}
ship_list.forEach((ship_obj) => {
var line = []
ship_obj["ordered_at"] = ord["created_at"]
ship_obj["order_id"] = ord["id"]
header.forEach((a) => {
var line_itm = a in ship_obj ? ship_obj[a] : ""
line.push(line_itm)
})
ship_arr.push(line)
})
}
return ship_arr
}
function makeRefundsArray(orders_arr) {
var refunds_arr = []
var header = getRefundHeaders()
for (let ord of orders_arr) {
var ref_list = ord.refunds
if (ref_list.length < 1) {
continue
}
ref_list.forEach((ref_obj) => {
var line = []
ref_obj["ordered_at"] = ord["created_at"]
if (ref_obj["transactions"].length == 1) {
var trans = ref_obj["transactions"][0]
ref_obj["transaction_id"] = trans["id"]
ref_obj["amount"] = trans["amount"]
ref_obj["kind"] = trans["kind"]
} else {
return
}
header.forEach((a) => {
var line_itm = a in ref_obj ? ref_obj[a] : ""
line.push(line_itm)
})
refunds_arr.push(line)
})
}
return refunds_arr
}
function appendRows(sht_name, arr) {
var sht = ss.getSheetByName(sht_name)
let last_row = sht.getLastRow()
sht.getRange(last_row + 1, 1, arr.length, arr[0].length).setValues(arr)
}
function processDataRange(sht_name) {
var sh = ss.getSheetByName(sht_name)
var data_rng = sh.getDataRange().offset(1, 0, sh.getLastRow() - 1)
var data = data_rng.getValues()
var targetData = new Array()
for (let n = 0; n < data.length; ++n) {
if (data[n].join().replace(/,/g, "") != "") {
targetData.push(data[n])
}
}
data_rng.clear()
targetData.sort((a, b) => a[1] - b[1])
sh.getRange(2, 1, targetData.length, targetData[0].length).setValues(
targetData
)
}
function removeDuplicates(obj_list, sht_name, id = "id") {
var sht = ss.getSheetByName(sht_name)
var sht_arr = sht.getDataRange().getValues()
for (let a = 0; a < sht_arr[0].length; a++) {
if (sht_arr[0][a] == id) {
var id_col = a
break
}
}
for (let x = 0; x < sht_arr.length; x++) {
var row_id = sht_arr[x][id_col]
for (let y = 0; y < obj_list.length; y++) {
if (obj_list[y][id_col] == row_id) {
sht.getRange(x + 1, 1, 1, sht_arr[0].length).clearContent()
break
}
}
}
}
function removeAllDuplicates(obj_list, sht_name, id = "id") {
var sht = ss.getSheetByName(sht_name)
var sht_arr = sht.getDataRange().getValues()
for (let a = 0; a < sht_arr[0].length; a++) {
if (sht_arr[0][a] == id) {
var id_col = a
break
}
}
for (let x = 0; x < sht_arr.length; x++) {
var row_id = sht_arr[x][id_col]
for (let y = 0; y < obj_list.length; y++) {
if (obj_list[y][id_col] == row_id) {
sht.getRange(x + 1, 1, 1, sht_arr[0].length).clearContent()
}
}
}
}
function getColumnList() {
var order_list = getOrderHeaders()
order_list.push(
line_items,
customer,
refunds,
discount_applications,
shipping_lines
)
return order_list
}
function getColumns(data: string) {
var sht = ss.getSheetByName(data)
if (sht == null) {
ss.insertSheet(data)
sht = ss.getSheetByName(data)
}
if (sht == null) {
throw new Error("unable to get/create sheet " + data)
}
var last_col = sht.getLastColumn()
var header_list = sht.getRange(1, 1, 1, last_col).getValues()
return header_list[0]
}
function getShippingHeaders() {
return getColumns(shipping_lines)
}
function getCustomerHeaders() {
return getColumns(customer)
}
function getLineItemHeaders() {
return getColumns(line_items)
}
function getOrderHeaders() {
return Order.columns
}
function getDiscountHeaders() {
return getColumns(discount_applications)
}
function getRefundHeaders() {
return getColumns(refunds)
}
function customerTable(orders_arr) {
var rng_arr = makeCustomerArray(orders_arr)
removeDuplicates(rng_arr, customer, "order_id")
SpreadsheetApp.flush()
appendRows(customer, rng_arr)
SpreadsheetApp.flush()
processDataRange(customer)
}
function discountsTable(orders_arr) {
var rng_arr = makeDiscountsArray(orders_arr)
if (rng_arr.length > 0) {
removeAllDuplicates(rng_arr, discount_applications, "order_id")
SpreadsheetApp.flush()
appendRows(discount_applications, rng_arr)
SpreadsheetApp.flush()
processDataRange(discount_applications)
}
}
function ordersTable(orders_arr) {
var rng_arr = makeArray(getOrderHeaders(), orders_arr)
removeDuplicates(rng_arr, orders, "id")
SpreadsheetApp.flush()
appendRows(orders, rng_arr)
SpreadsheetApp.flush()
processDataRange(orders)
}
function refundsTable(orders_arr) {
var rng_arr = makeRefundsArray(orders_arr)
if (rng_arr.length > 0) {
removeDuplicates(rng_arr, refunds, "id")
SpreadsheetApp.flush()
appendRows(refunds, rng_arr)
SpreadsheetApp.flush()
processDataRange(refunds)
}
}
function lineItemsTable(orders_arr) {
var rng_arr = makeLIArray(orders_arr)
removeDuplicates(rng_arr, line_items, "id")
SpreadsheetApp.flush()
appendRows(line_items, rng_arr)
SpreadsheetApp.flush()
processDataRange(line_items)
}
function shippingTable(orders_arr) {
var rng_arr = makeShippingArray(orders_arr)
if (rng_arr.length > 0) {
removeDuplicates(rng_arr, shipping_lines, "order_id")
SpreadsheetApp.flush()
appendRows(shipping_lines, rng_arr)
SpreadsheetApp.flush()
processDataRange(shipping_lines)
}
}
function unquote(value) {
if (value.charAt(0) == '"' && value.charAt(value.length - 1) == '"')
return value.substring(1, value.length - 1)
return value
}
function parseLinkHeader(header) {
var linkexp =
/<[^>]*>\s*(\s*;\s*[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g
var paramexp =
/[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g
var matches = header.match(linkexp)
var rels = {}
for (let i = 0; i < matches.length; i++) {
var split = matches[i].split(">")
var href = split[0].substring(1)
var ps = split[1]
var link = {
href: "",
rel: "",
}
link.href = href
var s = ps.match(paramexp)
for (let j = 0; j < s.length; j++) {
var p = s[j]
var paramsplit = p.split("=")
var name = paramsplit[0]
link[name] = unquote(paramsplit[1])
}
if (link.rel !== undefined) {
rels[link.rel] = link
}
}
return rels
}
export class Shop {
private shopifyApiKey: string
private shopifyApiSecretKey: string
private shopifyAdminApiAccessToken: string
private shopifyApiURI: string
static endpoints = {
orders: "/admin/api/2022-04/orders.json",
graphql: "/admin/api/2024-10/graphql.json",
}
constructor() {
let config = new Config()
this.shopifyApiKey = config.shopifyApiKey
this.shopifyApiSecretKey = config.shopifyApiSecretKey
this.shopifyAdminApiAccessToken = config.shopifyAdminApiAccessToken
this.shopifyApiURI = config.shopifyApiURI
}
RunOrders(start = "", end = "") {
if (start === "" || end === "") {
start = start_str
end = end_str
}
const date_exp = /^\d{4}[\-](0?[1-9]|1[012])[\-](0?[1-9]|[12][0-9]|3[01])$/
if (!date_exp.test(start) || !date_exp.test(end)) {
Logger.log("Dates must be in yyyy-MM-dd format")
return
}
this.GetOrders(start, end)
}
GetOrders(start: string, end: string) {
let endpoint = Shop.endpoints.orders
var params = {
created_at_min: start,
created_at_max: end,
fields: getColumnList(),
}
var done = false
var next_link = ""
do {
if (next_link === "") {
var response = this.shopifyAPI(endpoint, params)
console.log(response)
} else {
var response = this.shopifyAPI(endpoint, params, next_link)
console.log(response)
}
let resp = response.content
let headers = response.headers
var orders_arr: Order[] = resp["orders"]
if (orders_arr.length < 1) {
console.log("No orders found")
done = true
continue
}
var cols = getColumnList()
var numColumns = cols.length
var respKeys = Object.keys(orders_arr[0])
var respCols = respKeys.length
if (cols.filter((x) => respKeys.indexOf(x) === -1).length !== 0) {
Logger.log(
"Keys missing from return - " +
cols.filter((x) => respKeys.indexOf(x) === -1)
)
return
}
ordersTable(orders_arr)
refundsTable(orders_arr)
lineItemsTable(orders_arr)
customerTable(orders_arr)
discountsTable(orders_arr)
shippingTable(orders_arr)
if (headers["Link"] !== undefined) {
var links = parseLinkHeader(headers["Link"])
Logger.log(headers["Link"])
if (links["next"] === undefined) {
done = true
} else {
next_link = links["next"]["href"]
Logger.log(next_link)
}
} else {
done = true
}
} while (!done)
}
GetProducts() {
let done = false
let cursor = ""
let fields = ["id", "title"]
var response = {
content: {},
headers: {},
}
let products: Product[] = []
do {
let query = new ProductsQuery(fields, cursor)
response = this.shopifyGraphQLAPI(query.JSON)
console.log(response)
let productsResponse = new ProductsResponse(response.content)
if (productsResponse.products.edges.length <= 0) {
console.log("no products returned")
done = true
continue
}
for (let i = 0; i < productsResponse.products.edges.length; i++) {
let edge = productsResponse.products.edges[i]
console.log(JSON.stringify(edge))
let p = new Product()
Object.assign(edge.node, p)
products.push(p)
}
if (productsResponse.products.pageInfo.hasNextPage) {
cursor = productsResponse.products.pageInfo.endCursor
} else {
done = true
}
} while (!done)
}
shopifyAPI(endpoint: string, query: {}, next = "") {
var options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "get",
headers: {
"X-Shopify-Access-Token": this.shopifyAdminApiAccessToken,
},
}
if (next == "") {
var url = this.buildURL(endpoint, query)
var resp = UrlFetchApp.fetch(url, options)
console.log(resp.getContentText())
} else {
var url = next
var resp = UrlFetchApp.fetch(url, options)
console.log(resp.getContentText())
}
return {
content: JSON.parse(resp.getContentText()),
headers: resp.getHeaders(),
}
}
shopifyGraphQLAPI(query: {}, next = "") {
let endpoint = Shop.endpoints.graphql
let options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "post",
payload: JSON.stringify(query),
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": this.shopifyAdminApiAccessToken,
},
muteHttpExceptions: true,
}
var url = this.buildURL(endpoint)
console.log(UrlFetchApp.getRequest(url, options))
var resp = UrlFetchApp.fetch(url, options)
console.log(resp.getContentText())
return {
content: JSON.parse(resp.getContentText()),
headers: resp.getHeaders(),
}
}
buildURL(endpoint: string, params = {}) {
var url = this.shopifyApiURI + endpoint
if (params !== undefined && Object.keys(params).length > 0) {
let i = 0
for (let p in params) {
if (i === 0) {
url += "?" + p + "=" + encodeURIComponent(params[p])
} else {
url += "&" + p + "=" + encodeURIComponent(params[p])
}
i++
}
}
return url
}
}
export class Order {
static columns = [
"id",
"number",
"order_name",
"name",
"current_total_discounts",
"current_total_price",
"current_subtotal_price",
"current_total_tax",
"financial_status",
"landing_site",
"created_at",
"processed_at",
"processing_method",
"subtotal_price",
"source_url",
"tags",
"token",
"total_discounts",
"total_line_items_price",
"total_price",
"total_tax",
"updated_at",
]
constructor() {
for (let i = 0; i < Order.columns.length; i++) {
this[Order.columns[i]] = ""
}
}
}
export class Product {
body_html: string
created_at: Date
handle: string
id: number
images: ProductImage[]
options: ProductOption[]
product_type: string
published_at: Date
published_scope: string
status: string
tags: string[]
template_suffix: string
title: string
updated_at: Date
variants: ProductVariant[]
vendor: string
}
class ProductImage {
id: number
product_id: number
position: number
created_at: Date
updated_at: Date
width: number
height: number
src: string
variant_ids: number[]
}
class ProductOption {
id: number
product_id: number
name: string
position: number
values: string[]
}
class ProductVariant {
barcode: string
compare_at_price: number
created_at: Date
fulfillment_service: string
grams: number
weight: number
weight_unit: string
id: number
inventory_item_id: number
inventory_management: string
inventory_policy: string
inventory_quantity: number
option1: string
position: number
price: number
product_id: number
requires_shipping: boolean
sku: string
taxable: boolean
title: string
updated_at: Date
}
class Products {
edges: ProductEdge[]
pageInfo: PageInfo
}
class ProductEdge {
node: Product
cursor: string
}
class PageInfo {
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string
endCursor: string
}
class ProductsQuery {
GQL: string
JSON: JSON
constructor(
fields: string[] = ["id", "title", "handle"],
cursor: string = ""
) {
let cursorText: string
if (cursor == "") {
cursorText = ""
} else {
cursorText = `, after: "${cursor}"`
}
this.GQL = `{
products(first: 10${cursorText}) {
edges {
node { ${fields.join(" ")} }
}
pageInfo {
hasNextPage
endCursor
}
}
}`
let j = `{"query": ${formatGqlForJSON(this.GQL)}}`
console.log(j)
this.JSON = JSON.parse(j)
}
}
class ProductsResponse {
products: Products
constructor(response: {}) {
this.products = response["data"]["products"]
}
}
function formatGqlForJSON(gql: string) {
let singleLine = gql.split("\n").join(" ").replace(/\s+/g, " ")
return JSON.stringify(singleLine)
}