Significant performance improvements to the 'Loading media...' phase: - Reduced client-server round trips by consolidating the initial handshake (diagnostics + media fetch) into a single backend call: getMediaManagerInitialState. - Implemented batched Google Drive metadata retrieval in GASDriveService using the Advanced Drive API, eliminating per-file property fetching calls. - Switched to HtmlService templates in showMediaManager to pass initial SKU/Title data directly, enabling the UI shell to appear instantly upon opening. - Updated documentation (ARCHITECTURE.md, MEMORY.md) to clarify Webpack global assignment requirements for GAS functions. - Verified with comprehensive updates to unit and integration tests.
8.9 KiB
Architecture Documentation
System Overview
This project serves as a bridge between Google Sheets and Shopify. It enables a two-way sync (primarily Sheets to Shopify for products) and allows managing inventory directly from a spreadsheet.
Core Flows
-
Product Updates:
- User edits a cell in the "product_inventory" sheet.
onEditQueuetrigger fires, capturing the SKU and timestamp.- Edits are batched in
PropertiesService(script properties). - A time-based trigger runs
processBatchedEditsevery minute. - The processing function locks the script, reads the queue, and pushes changes to Shopify via the Admin API.
-
Order Sync:
- Users can run menu commands to fetch orders from Shopify.
- The
Shopclass fetches orders via the REST API, handling pagination. - Data is populated into specific sheets (
_orders,_line_items,_customer, etc.).
Key Components
1. Queue System (src/onEditQueue.ts)
To avoid hitting Shopify API rate limits and Google Apps Script execution time limits, edits are not processed immediately.
-
onEditQueue(e):- Triggered on every cell edit.
- Checks if the edit is valid (correct sheet, valid SKU).
- Acquires a
DocumentLock. - Updates a JSON list in
ScriptProperties(pendingEdits). - Debounces edits (updates timestamp if SKU is already pending).
-
processBatchedEdits():- Run via time-based trigger (every 1 minute).
- Acquires a
ScriptLock. - Reads
pendingEdits. - Filters for edits older than
BATCH_INTERVAL_MS(30s) to allow for multiple quick edits to the same SKU. - Iterates through valid edits and calls
Product.UpdateShopifyProduct. - SKU Validation: Before any action, checks if the SKU is valid (not empty,
?, orn). Aborts if invalid.
2. Product Lifecycle Logic
-
Creation:
- Uses the SKU as the Shopify Handle (URL slug).
- Prevents creation if the SKU is a placeholder.
-
Updates:
- Prioritizes ID-based lookup.
- If the
shopify_idcolumn is populated, the system trusts this ID to locate the product in Shopify, even if the SKU has changed in the sheet. - As a result, changing a SKU in the sheet and syncing will rename the existing product (handle/SKU) rather than creating a duplicate.
2. Shopify Integration (src/shopifyApi.ts)
The project uses a hybrid approach for the Shopify Admin API:
-
REST API: Used primarily for fetching Orders (legacy support).
-
GraphQL API: Used for fetching and updating Products and Inventory.
-
GraphQL API: Used for fetching and updating Products and Inventory.
The Shop class implements the IShop interface, handling authentication using credentials stored in the "vars" sheet. The interface decoupling facilitates robust unit testing via MockShop.
3. Configuration (src/config.ts)
Configuration, including API keys, is stored in a dedicated Google Sheet named "vars". The Config class reads these values at runtime using a vlookup style helper.
Required "vars" columns:
key: The name of the configuration variable.value: The actual value.
4. Global Entry Points (src/global.ts)
Because Webpack bundles the code into an IIFE (Immediately Invoked Function Expression) to avoid global scope pollution, top-level functions defined in modules are not automatically globally accessible in the Apps Script environment.
- Requirement: Any function that needs to be called from the frontend via
google.script.run, triggered by a menu, or attached to a spreadsheet event must be explicitly assigned to theglobalobject insrc/global.ts. - Example:
import { myFunc } from "./myModule" ;(global as any).myFunc = myFunc - Rationale: This is the only way for the Google Apps Script runtime to find these functions when they are invoked via the
google.script.runAPI or other entry point mechanisms.
5. Status Automation (src/statusHandlers.ts)
A modular system handles changes to the status column. It uses a registry of StatusHandler implementations:
- Published: Sets Shopify Status
ACTIVE, Quantity1. - Sold/Artist Swap: Sets Shopify Status
ACTIVE, Quantity0. - Drafted: Sets Shopify Status
DRAFT.
Triggers
Triggers are managed programmatically via src/triggers.ts. Running reinstallTriggers will wipe existing project triggers and set up the standard set:
onEdit->onEditHandler(Main Router)TimeBased (1 min)->processBatchedEditsTimeBased (10 min)->checkRecentSales
5. Troubleshooting Panel (src/sidebar.ts, src/Sidebar.html)
A dedicated side panel provides visibility into the background queue system.
-
Backend (
src/sidebar.ts):getQueueStatus(): Returns the current state of the queue and global toggle.setQueueEnabled(): Toggles the globalqueueEnabledscript property.deleteEdit()/pushEdit(): Manages specific items in the queue with safety checks.
-
Frontend (
src/Sidebar.html):- Displays pending edits with timestamps.
- Provides controls to globally enable/disable processing.
- Allows manual intervention (delete/push) for individual items.
6. Service Layer, Testing & Quality
To enable unit testing without Google Apps Script dependencies, the project uses a Service pattern with Dependency Injection.
Architecture
ISpreadsheetService: Interface for all sheet interactions.GASSpreadsheetService: Production implementation wrappingSpreadsheetApp.MockSpreadsheetService: In-memory implementation for tests.
Quality Assurance
We use Husky and lint-staged to enforce quality standards at the commit level:
- Pre-commit Hook: Automatically runs
npm test -- --onlyChanged --coverage. - Coverage Policy: Any file modified in a commit must meet an 80% line coverage threshold. This ensures the codebase quality improves monotonically ("Boy Scout Rule").
Classes (like Product) should accept an ISpreadsheetService in their constructor. This allows providing the Mock service during tests to verify logic without touching real Google Sheets.
7. Media Manager (src/mediaHandlers.ts, src/MediaSidebar.html)
We implemented a "Sidebar-First" architecture for product media to handle the complexity of Google Picker and Shopify Sync.
Frontend (MediaSidebar.html)
- Glassmorphism UI: Uses modern CSS for a premium feel.
- Polling: Since the sidebar can't listen to Sheet events directly efficiently, it polls
getMediaState(sku)to detect when the user selects a different product row. - Google Picker API:
- Uses the New Google Photos Picker (Session-based) for selecting photos.
- Uses the Google Drive Picker (Legacy) for selecting existing Drive files.
- Handles OAuth token passing securely from the server side (
google.script.run).
Backend (mediaHandlers.ts)
- Import Strategy:
- Safe Zone: Files are first downloaded/copied to the Drive Root to ensure we have the asset.
- Move: Then they are moved to the organized SKU folder (
/Product Photos/[SKU]/). - Resilience: The file creation logic tries multiple methods (Standard
DriveApp, Sanitized Blob, AdvancedDriveAPI) to handle the notoriously fickle nature of UrlFetchApp blobs.
- Shopify Sync:
MediaServicemanages the state.- Calculates checksums to avoid re-uploading duplicate images.
- Uses Shopify's "Staged Uploads" -> "Create Media" mutation flow.
8. Apps Script & HTML Service Constraints
When working with HtmlService (client-side code), the environment differs significantly from the server-side V8 runtime.
-
Server-Side (
.ts/.gs):- Runtime: V8 Engine.
- Syntax: Modern ES6+ (Classes, Arrow Functions,
const/let) is fully supported. - Recommendation: Use standard TypeScript patterns.
-
Client-Side (
.htmlserved viacreateHtmlOutputFromFile):- Runtime: Legacy Browser Environment / Strict Caja Sanitization.
- Constraint: The parser often chokes on ES6
classsyntax and complex template strings inside HTML attributes. - Rule 1: NO ES6 CLASSES. Use ES5
functionconstructors andprototypemethods. - Rule 2: NO Complex Template Strings in Attributes. Do not use
src="${var}"if the variable contains a URL. Usedocument.createElementand set properties (e.g.,element.src = value) programmatically. - Rule 3: Unified Script Tags. Consolidate scripts into a single block where possible to avoid parser merge errors.
- Rule 4: Var over Let/Const. Top-level variables should use
varor explicitwindowassignment to ensure they are accessible to inline HTML handlers (e.g.,onclick="handler()").