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.
168 lines
8.9 KiB
Markdown
168 lines
8.9 KiB
Markdown
# 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
|
|
|
|
1. **Product Updates**:
|
|
- User edits a cell in the "product_inventory" sheet.
|
|
- `onEditQueue` trigger fires, capturing the SKU and timestamp.
|
|
- Edits are batched in `PropertiesService` (script properties).
|
|
- A time-based trigger runs `processBatchedEdits` every minute.
|
|
- The processing function locks the script, reads the queue, and pushes changes to Shopify via the Admin API.
|
|
|
|
2. **Order Sync**:
|
|
- Users can run menu commands to fetch orders from Shopify.
|
|
- The `Shop` class 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, `?`, or `n`). 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_id` column 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 the `global` object in `src/global.ts`.
|
|
- **Example**:
|
|
```typescript
|
|
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.run` API 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`, Quantity `1`.
|
|
- **Sold/Artist Swap**: Sets Shopify Status `ACTIVE`, Quantity `0`.
|
|
- **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)` -> `processBatchedEdits`
|
|
- `TimeBased (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 global `queueEnabled` script 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 wrapping `SpreadsheetApp`.
|
|
- **`MockSpreadsheetService`**: In-memory implementation for tests.
|
|
|
|
#### Quality Assurance
|
|
We use **Husky** and **lint-staged** to enforce quality standards at the commit level:
|
|
1. **Pre-commit Hook**: Automatically runs `npm test -- --onlyChanged --coverage`.
|
|
2. **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, Advanced `Drive` API) to handle the notoriously fickle nature of UrlFetchApp blobs.
|
|
- **Shopify Sync**:
|
|
- `MediaService` manages 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.
|
|
|
|
1. **Server-Side (`.ts`/`.gs`)**:
|
|
- **Runtime**: V8 Engine.
|
|
- **Syntax**: Modern ES6+ (Classes, Arrow Functions, `const`/`let`) is fully supported.
|
|
- **Recommendation**: Use standard TypeScript patterns.
|
|
|
|
2. **Client-Side (`.html` served via `createHtmlOutputFromFile`)**:
|
|
- **Runtime**: Legacy Browser Environment / Strict Caja Sanitization.
|
|
- **Constraint**: The parser often chokes on ES6 `class` syntax and complex template strings inside HTML attributes.
|
|
- **Rule 1**: **NO ES6 CLASSES**. Use ES5 `function` constructors and `prototype` methods.
|
|
- **Rule 2**: **NO Complex Template Strings in Attributes**. Do not use `src="${var}"` if the variable contains a URL. Use `document.createElement` and 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 `var` or explicit `window` assignment to ensure they are accessible to inline HTML handlers (e.g., `onclick="handler()"`).
|