Compare commits

...

4 Commits

Author SHA1 Message Date
a5f9b1542c add queued and batched edits of products 2025-09-07 23:28:13 -06:00
688536d0ac add Shopify category to product 2025-08-31 20:22:30 -06:00
6d75973835 simplify product category handling 2025-08-10 13:43:54 -06:00
62514fa20e simplify and update onEdit columns 2025-08-10 02:09:52 -06:00
7 changed files with 159 additions and 36 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
**/node_modules/** **/node_modules/**
dist/** dist/**
desktop.ini desktop.ini
.continue/**

View File

@ -43,37 +43,24 @@ export function columnOnEditHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
for (let row = e.range.getRow(); row <= e.range.getLastRow(); row++) { for (let row = e.range.getRow(); row <= e.range.getLastRow(); row++) {
console.log("row: " + row) console.log("row: " + row)
let updateString = "updating " + header + " on row " + row let updateString = "updating " + header + " on row " + row
switch (header) { let shopifyUpdateColumns = [
case "shopify_status": "shopify_status",
toastAndLog(updateString) "title",
updateProductToShopify(row) "product_type",
break "tags",
case "title": "description",
toastAndLog(updateString) "base_price",
updateProductToShopify(row) "original_price",
break "weight_grams",
case "product_type": "product_height_cm",
toastAndLog(updateString) "product_width_cm",
updateProductToShopify(row) "product_depth_cm"
break ]
case "tags": if (shopifyUpdateColumns.includes(header)) {
toastAndLog(updateString) // Accumulate changes for 30s before updating
updateProductToShopify(row) toastAndLog(updateString)
break updateProductToShopify(row)
case "description": break
toastAndLog(updateString)
updateProductToShopify(row)
break
case "price":
toastAndLog(updateString)
updateProductToShopify(row)
break
case "compare_at_price":
toastAndLog(updateString)
updateProductToShopify(row)
break
default:
continue
} }
toastAndLog("completed " + updateString) toastAndLog("completed " + updateString)
} }

View File

@ -12,7 +12,7 @@ import {
formatGqlForJSON, formatGqlForJSON,
} from "./shopifyApi" } from "./shopifyApi"
import * as shopify from 'shopify-admin-api-typings' import * as shopify from 'shopify-admin-api-typings'
import { getCellRangeByColumnName, getRowByColumnValue } from "./sheetUtils" import { getCellRangeByColumnName, getRowByColumnValue, vlookupByColumns } from "./sheetUtils"
import { Config } from "./config" import { Config } from "./config"
@ -114,10 +114,12 @@ export class Product {
) )
} }
ShopifyCategory(): string {
return vlookupByColumns("values", "product_type", this.product_type, "shopify_category")
}
ToShopifyProductSet() { ToShopifyProductSet() {
let sps = new ShopifyProductSetInput() let sps = new ShopifyProductSetInput()
//TODO: map category IDs
//sps.category = this.category
if (this.shopify_id != "") { if (this.shopify_id != "") {
sps.id = this.shopify_id sps.id = this.shopify_id
} }
@ -125,6 +127,10 @@ export class Product {
sps.status = this.shopify_status sps.status = this.shopify_status
} }
sps.productType = this.product_type sps.productType = this.product_type
let category = this.ShopifyCategory()
if (category !== "") {
sps.category = this.ShopifyCategory()
}
sps.tags = this.tags sps.tags = this.tags
sps.title = this.title sps.title = this.title
sps.descriptionHtml = this.description sps.descriptionHtml = this.description

View File

@ -14,6 +14,10 @@ import { createMissingPhotoFolders } from "./createMissingPhotoFolders"
import { reinstallTriggers } from "./triggers" import { reinstallTriggers } from "./triggers"
import { newSkuHandler } from "./newSku" import { newSkuHandler } from "./newSku"
import { columnOnEditHandler } from "./OnEditHandler" import { columnOnEditHandler } from "./OnEditHandler"
import {
onEditQueue,
processBatchedEdits
} from "./onEditQueue"
import { fillProductFromTemplate } from "./fillProductFromTemplate" import { fillProductFromTemplate } from "./fillProductFromTemplate"
// prettier-ignore // prettier-ignore
@ -25,6 +29,8 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
;(global as any).matchProductToShopifyOnEditHandler = matchProductToShopifyOnEditHandler ;(global as any).matchProductToShopifyOnEditHandler = matchProductToShopifyOnEditHandler
;(global as any).updateShopifyProductHandler = updateShopifyProductHandler ;(global as any).updateShopifyProductHandler = updateShopifyProductHandler
;(global as any).columnOnEditHandler = columnOnEditHandler ;(global as any).columnOnEditHandler = columnOnEditHandler
;(global as any).onEditQueue = onEditQueue
;(global as any).processBatchedEdits = processBatchedEdits
;(global as any).reauthorizeScript = reauthorizeScript ;(global as any).reauthorizeScript = reauthorizeScript
;(global as any).reinstallTriggers = reinstallTriggers ;(global as any).reinstallTriggers = reinstallTriggers
;(global as any).newSkuHandler = newSkuHandler ;(global as any).newSkuHandler = newSkuHandler

View File

@ -5,6 +5,7 @@ import {
getColumnValuesByName, getColumnValuesByName,
} from "./sheetUtils" } from "./sheetUtils"
const LOCK_TIMEOUT_MS = 1000 * 10
export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) { export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
var sheet = SpreadsheetApp.getActive().getActiveSheet() var sheet = SpreadsheetApp.getActive().getActiveSheet()
@ -20,7 +21,14 @@ export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
console.log("new ID was not requested, returning") console.log("new ID was not requested, returning")
return return
} }
newSku(row) // Acquire a user lock to prevent multiple onEdit calls from clashing
const documentLock = LockService.getDocumentLock()
try {
documentLock.waitLock(LOCK_TIMEOUT_MS)
newSku(row)
} finally {
documentLock.releaseLock()
}
} }
export function newSku(row: number) { export function newSku(row: number) {

109
src/onEditQueue.ts Normal file
View File

@ -0,0 +1,109 @@
import { getCellValueByColumnName } from "./sheetUtils"
import { Product } from "./Product"
import { Shop } from "./shopifyApi"
// --- Constants ---
const BATCH_INTERVAL_MS = 30 * 1000 // 30 seconds
const LOCK_TIMEOUT_MS = 10 * 1000 // 10 seconds for lock acquisition
const CACHE_KEY_EDITS = "pendingEdits"
const CACHE_KEY_LAST_EDIT_TIME = "lastEditTime"
const SCRIPT_PROPERTY_TRIGGER_SCHEDULED = "batchTriggerScheduled"
export function onEditQueue(e) {
const sheet = e.source.getActiveSheet()
if (sheet.getName() !== "product_inventory") {
console.log("skipping edit on sheet " + sheet.getName())
return
}
const range = e.range
const row = range.getRow()
const sku = getCellValueByColumnName(sheet, "sku", row)
if (!sku) {
console.log("No SKU found for row " + row)
return
}
// Acquire a user lock to prevent multiple onEdit calls from clashing
const documentLock = LockService.getDocumentLock()
try {
documentLock.waitLock(LOCK_TIMEOUT_MS)
const scriptProperties = PropertiesService.getScriptProperties() // Shared cache for all users
// 1. Accumulate edits in cache
let pendingEdits = []
try {
pendingEdits = JSON.parse(
scriptProperties.getProperty(CACHE_KEY_EDITS) || "[]"
)
} catch (e) {
console.log("Cache corruption: " + e.message)
scriptProperties.setProperty(CACHE_KEY_EDITS, "[]")
}
const existingIndex = pendingEdits.findIndex((item) => item.sku === sku)
if (existingIndex !== -1) {
console.log("New edit on queued SKU '"+sku+"', resetting timer...")
pendingEdits[existingIndex].timestamp = Date.now()
} else {
console.log("New SKU '"+sku+"' added to queue.")
pendingEdits.push({ sku, timestamp: Date.now() })
}
scriptProperties.setProperty(CACHE_KEY_EDITS, JSON.stringify(pendingEdits))
} catch (error) {
console.log(
"Error in onEdit (lock acquisition or cache operation): " + error.message
)
} finally {
documentLock.releaseLock()
}
}
export function processBatchedEdits() {
const scriptLock = LockService.getScriptLock() // Use script lock for the processing function
try {
scriptLock.waitLock(LOCK_TIMEOUT_MS)
const scriptProperties = PropertiesService.getScriptProperties()
let pendingEdits = []
try {
const pendingEditsStr = scriptProperties.getProperty(CACHE_KEY_EDITS)
pendingEdits = pendingEditsStr ? JSON.parse(pendingEditsStr) : []
} catch (e) {
console.log("Cache corruption: " + e.message)
scriptProperties.setProperty(CACHE_KEY_EDITS, "[]")
}
console.log(`Total SKUs in queue: ${pendingEdits.length}`)
const now = Date.now()
const toProcess = pendingEdits.filter(
(edit) => now - edit.timestamp > BATCH_INTERVAL_MS
)
if (toProcess.length > 0) {
let shop = new Shop()
console.log(`Processing ${toProcess.length} SKUs...`)
toProcess.forEach((edit) => {
console.log(
`Processing SKU ${edit.sku}, Timestamp: ${new Date(edit.timestamp)}`
)
let p = new Product(edit.sku)
p.UpdateShopifyProduct(shop)
})
pendingEdits = pendingEdits.filter(
edit => !toProcess.some(p => p.sku === edit.sku)
);
scriptProperties.setProperty(CACHE_KEY_EDITS, JSON.stringify(pendingEdits));
console.log(`Processed ${toProcess.length} edits.`)
} else {
console.log("No pending edits to process.")
}
} catch (error) {
console.log(
"Error in processBatchedEdits (lock acquisition or processing): " +
error.message
)
} finally {
scriptLock.releaseLock()
}
}

View File

@ -10,5 +10,11 @@ export function reinstallTriggers() {
.forSpreadsheet(ss) .forSpreadsheet(ss)
.onEdit() .onEdit()
.create() .create()
ScriptApp.newTrigger("columnOnEditHandler").forSpreadsheet(ss).onEdit().create() // ScriptApp.newTrigger("columnOnEditHandler").forSpreadsheet(ss).onEdit().create()
// ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create()
ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create()
ScriptApp.newTrigger("processBatchedEdits")
.timeBased()
.everyMinutes(1)
.create()
} }