Files
product_inventory/src/shopifyApi.ts
Ben Miller 7c35817313 Refactor Media Manager sync logic and fix duplication bugs
This major refactor addresses improper image matching and duplication:

- Implemented strict ID-based matching in 'MediaService', removing the greedy filename matching fallback.

- Redesigned synchronization pipeline to treat Google Drive as the Source of Truth, supporting orphan adoption (Shopify -> Drive) and secure uploads.

- Implemented 'gallery_order' using Drive file properties (supporting both v2 and v3 APIs) for stable, drag-and-drop global ordering.

- Added conditional file renaming using timestamps to enforce '_' naming convention without unnecessary renames.

- Fixed runtime errors in 'MediaService' loops and updated 'ShopifyMediaService' GraphQL mutations to match correctly schema.

- Rewrote 'MediaService.test.ts' with robust test cases for strict matching, adoption, sorting, and reordering.
2025-12-28 12:25:13 -07:00

1233 lines
32 KiB
TypeScript

// 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"
import * as shopify from "shopify-admin-api-typings"
import gql from 'graphql-tag'
import { IShop } from "./interfaces/IShop"
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 implements IShop {
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)
}
FetchOrders(start: Date, end: Date) {
let endpoint = Shop.endpoints.orders
let start_str = start.toISOString()
let end_str = end.toISOString()
var params = {
created_at_min: start_str,
created_at_max: end_str,
fields: "id,created_at,financial_status,name,line_items,subtotal_price,total_price,total_tax",
status: "any"
}
var all_orders: any[] = []
var done = false
var next_link = ""
do {
var response
if (next_link === "") {
response = this.shopifyAPI(endpoint, params)
} else {
response = this.shopifyAPI(endpoint, params, next_link)
}
let resp = response.content
let headers = response.headers
var orders_arr = resp["orders"]
if (orders_arr.length > 0) {
all_orders = all_orders.concat(orders_arr)
}
if (headers["Link"] !== undefined) {
var links = parseLinkHeader(headers["Link"])
if (links["next"] === undefined) {
done = true
} else {
next_link = links["next"]["href"]
}
} else {
done = true
}
} while (!done)
return all_orders
}
GetProducts() {
let done = false
let query = ""
let cursor = ""
let fields = ["id", "title"]
var response = {
content: {},
headers: {},
}
let products: ShopifyProduct[] = []
do {
let pq = new ShopifyProductsQuery(query, fields, cursor)
response = this.shopifyGraphQLAPI(pq.JSON)
console.log(response)
let productsResponse = new ShopifyProductsResponse(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 ShopifyProduct()
Object.assign(edge.node, p)
products.push(p)
}
if (productsResponse.products.pageInfo.hasNextPage) {
cursor = productsResponse.products.pageInfo.endCursor
} else {
done = true
}
} while (!done)
}
GetProductBySku(sku: string) {
console.log("GetProductBySku('" + sku + "')")
let gql = /* GraphQL */ `
query productBySku {
products(first: 1, query: "sku:${sku}") {
edges {
node {
id
title
handle
variants(first: 1) {
nodes {
id
sku
}
}
options {
id
name
optionValues {
id
name
}
}
}
}
}
}
`
let query = buildGqlQuery(gql, {})
let response = this.shopifyGraphQLAPI(query)
if (response.content.data.products.edges.length <= 0) {
console.log("GetProductBySku: no product matched")
return
}
let product = response.content.data.products.edges[0].node
console.log("Product found:\n" + JSON.stringify(product, null, 2))
return product
}
GetProductById(id: string) {
console.log("GetProductById('" + id + "')")
let gql = /* GraphQL */ `
query productById {
product(id: "${id}") {
id
title
handle
variants(first: 1) {
nodes {
id
sku
}
}
}
}
`
let query = buildGqlQuery(gql, {})
let response = this.shopifyGraphQLAPI(query)
if (!response.content.data.product) {
console.log("GetProductById: no product matched")
return null;
}
let product = response.content.data.product
console.log("Product found:\n" + JSON.stringify(product, null, 2))
return product
}
GetInventoryItemBySku(sku: string) {
console.log('GetInventoryItemBySku("' + sku + '")')
let gql = /* GraphQL */ `
query inventoryItems {
inventoryItems(first:1, query:"sku:${sku}") {
edges {
node {
id
tracked
sku
}
}
}
}
`
let query = buildGqlQuery(gql, {})
let response = this.shopifyGraphQLAPI(query)
let item: shopify.InventoryItem =
response.content.data.inventoryItems.edges[0].node
console.log(
"GetInventoryItemBySku: found item:\n" + JSON.stringify(item, null, 2)
)
return item
}
UpdateInventoryItemQuantity(
item: shopify.InventoryItem,
delta: number = 1,
config: Config
) {
console.log("UpdateInventoryItemQuantity(" + JSON.stringify(item) + ")")
let gql = /* GraphQL */ `
mutation inventoryAdjustQuantities(
$input: InventoryAdjustQuantitiesInput!
) {
inventoryAdjustQuantities(input: $input) {
userErrors {
field
message
}
inventoryAdjustmentGroup {
createdAt
reason
referenceDocumentUri
changes {
name
delta
}
}
}
}
`
let variables = {
input: {
reason: "correction",
name: "available",
changes: [
{
delta: delta,
inventoryItemId: item.id,
locationId: config.shopifyLocationId,
},
],
},
}
let query = buildGqlQuery(gql, variables)
let response = this.shopifyGraphQLAPI(query)
let newItem: shopify.InventoryItem = response.content
console.log("new item:\n" + JSON.stringify(newItem, null, 2))
return newItem
}
SetInventoryItemQuantity(
item: shopify.InventoryItem,
quantity: number,
config: Config
) {
console.log("SetInventoryItemQuantity(" + JSON.stringify(item) + ", " + quantity + ")")
let gql = /* GraphQL */ `
mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) {
inventorySetQuantities(input: $input) {
inventoryAdjustmentGroup {
changes {
name
delta
}
}
userErrors {
field
message
}
}
}
`
let variables = {
input: {
name: "available",
reason: "correction",
ignoreCompareQuantity: true,
quantities: [
{
inventoryItemId: item.id,
locationId: config.shopifyLocationId,
quantity: quantity,
},
],
},
}
let query = buildGqlQuery(gql, variables)
let response = this.shopifyGraphQLAPI(query)
// Response structure is different for setQuantities
console.log("SetInventoryItemQuantity response:\n" + JSON.stringify(response, null, 2))
return response.content
}
SetInventoryItemDefaults(item: shopify.InventoryItem, config: Config) {
let gql = /* GraphQL */ `
mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
unitCost {
amount
}
tracked
countryCodeOfOrigin
provinceCodeOfOrigin
harmonizedSystemCode
countryHarmonizedSystemCodes(first: 1) {
edges {
node {
harmonizedSystemCode
countryCode
}
}
}
}
userErrors {
message
}
}
}
`
let variables = {
id: item.id,
input: {
tracked: true,
countryCodeOfOrigin: config.shopifyCountryCodeOfOrigin,
provinceCodeOfOrigin: config.shopifyProvinceCodeOfOrigin,
},
}
let query = buildGqlQuery(gql, variables)
let response = this.shopifyGraphQLAPI(query)
let newItem: shopify.InventoryItem = response.content
return newItem
}
SetInventoryItemWeight(item: shopify.InventoryItem, config: Config, weight: number, weight_unit: shopify.WeightUnit) {
let gql = /* GraphQL */ `
mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
measurement {
weight {
value
unit
}
}
}
userErrors {
field
message
}
}
}
`
let variables = {
id: item.id,
input: {
measurement: {
weight: {
value: weight,
unit: weight_unit
}
}
},
}
let query = buildGqlQuery(gql, variables)
let response = this.shopifyGraphQLAPI(query)
let newItem: shopify.InventoryItem = response.content
return newItem
}
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 = "") {
console.log("shopifyGraphQLAPI:query: " + JSON.stringify(query))
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(
"shopifyGraphQLAPI sending request:\n" +
JSON.stringify(UrlFetchApp.getRequest(url, options), null, 2)
)
var resp = UrlFetchApp.fetch(url, options)
let content = resp.getContentText()
console.log("shopifyGraphQLAPI got response:\n" + content)
let content_json = JSON.parse(content)
console.log(JSON.stringify(content_json, null, 2))
return {
content: content_json,
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
}
getShopDomain(): string {
// Extract from https://{shop}.myshopify.com
return this.shopifyApiURI.replace('https://', '').replace(/\/$/, '');
}
}
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 ShopifyProduct {
body_html: string
created_at: Date
handle: string
id: string
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: ShopifyVariantNodes
vendor: string
}
export class ShopifyVariantNodes {
nodes: ShopifyProductVariant[]
}
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: string
product_id: number
name: string
position: number
values: string[]
optionValues?: ShopifyProductOptionValues
}
export class ShopifyProductOptionValues {
id?: string
name?: string
}
export class ShopifyVariantOptionValueInput {
id?: string
linkedMetafieldValue?: string
name?: string
optionId?: string
optionName?: string
}
export class ShopifyProductVariant {
barcode: string
compare_at_price: number
created_at: Date
fulfillment_service: string
grams: number
weight: number
weight_unit: string
id: string
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: ShopifyProduct
variants?: ShopifyVariants
options?: ShopifyProductOption[]
cursor: string
}
export class ShopifyProductOption {
id?: string
name?: string
optionValues?: ShopifyProductOptionValue[]
values?: ShopifyProductOptionValue[]
}
export class ShopifyProductOptionValue {
id?: string
name?: string
}
export class ShopifyVariants {
nodes?: ShopifyVariant[]
node?: ShopifyVariant
}
export class ShopifyVariant {
id?: string
sku?: string
price?: number
compareAtPrice?: number
barcode?: string
position?: number
metafields?: shopify.Metafield[]
inventory_item?: shopify.InventoryItemInput
nodes?: ShopifyProductVariant[]
//TODO: support multiple options
optionValues: [{}] = [
{
optionName: "Title",
name: "Default Title",
},
]
}
class PageInfo {
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string
endCursor: string
}
export class ShopifyProductsQuery {
GQL: string
JSON: JSON
constructor(
query: string = "",
fields: string[] = ["id", "title", "handle"],
cursor: string = "",
pageSize: number = 10
) {
let cursorText: string
if (cursor == "") {
cursorText = ""
} else {
cursorText = `, after: "${cursor}"`
}
let queryText: string
if (query == "") {
queryText = ""
} else {
queryText = `, query: "${query}"`
}
this.GQL = /* GraphQL */ `{
products(first: ${pageSize}${cursorText}${queryText}) {
edges {
node {
${fields.join(" ")}
variants(first:1) {
nodes {
id
}
}
options {
id
name
optionValues {
id
name
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}`
let j = `{"query": ${formatGqlForJSON(this.GQL)}}`
console.log(j)
this.JSON = JSON.parse(j)
}
}
export class ShopifyProductsResponse {
products: Products
constructor(response: {}) {
this.products = response["data"]["products"]
}
}
export class ShopifyProductSetQuery {
GQL = /* GraphQL */ `
mutation setProduct($productSet: ProductSetInput!) {
productSet(input: $productSet) {
product {
id
}
productSetOperation {
id
status
userErrors {
code
field
message
}
}
userErrors {
code
field
message
}
}
}
`
JSON: JSON
constructor(product: ShopifyProductSetInput, synchronous: boolean = true) {
let j = `{
"query": ${formatGqlForJSON(String(this.GQL))},
"variables": {
"productSet": ${JSON.stringify(product)},
"synchronous": ${synchronous}
}
}`
console.log(j)
this.JSON = JSON.parse(j)
}
}
export class ShopifyProductSetInput {
category: string
descriptionHtml: string
handle: string
id?: string
productType: string
redirectNewHandle: boolean = true
status: string = "DRAFT"
tags: string
title: string
vendor: string
variants: ShopifyVariant[]
metafields: shopify.MetafieldInput[]
//TODO: Support multiple product options
productOptions: [{}] = [
{
name: "Title",
values: {
name: "Default Title",
},
},
]
}
export class ProductVariantSetInput {
barcode: string
compareAtPrice: number
id: string
optionValues: VariantOptionValueInput[] = []
position?: number
price?: number
requiresComponents?: boolean
sku?: string
taxable?: boolean
taxCode?: string
}
export class VariantOptionValueInput {
id?: string
linkedMetafieldValue?: string
name?: string
optionId?: string
optionName?: string
}
export function formatGqlForJSON(gql: string) {
let singleLine = gql.split("\n").join(" ").replace(/\s+/g, " ")
return JSON.stringify(singleLine)
}
export function runShopifyOrders() {
let shop = new Shop()
shop.RunOrders()
}
export function getShopifyProducts() {
let shop = new Shop()
shop.GetProducts()
}
export function buildGqlQuery(gql: string, variables: {}) {
let query = `{
"query": ${formatGqlForJSON(String(gql))},
"variables": ${JSON.stringify(variables)}
}`
console.log("buildGqlQuery:\n" + query)
return JSON.parse(query)
}