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": {
"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
shopifyApiKey: string
shopifyApiSecretKey: string
shopifyAdminApiAccessToken: string
shopifyApiURI: string
constructor() {
let ss = SpreadsheetApp.getActive()
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() {
let ui = SpreadsheetApp.getUi()
ui.createMenu("BLM")
.addItem("Fill out product from template", "fillProductFromTemplate")
.addItem("Create missing photo folders", "createMissingPhotoFolders")
.addSeparator()
.addItem("Run Shopify Orders", "runShopifyOrders")
.addItem("Get Shopify Products", "getShopifyProducts")
.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)
}