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.
This commit is contained in:
Ben Miller
2025-12-28 12:25:13 -07:00
parent 6e1222cec9
commit 7c35817313
14 changed files with 1299 additions and 505 deletions

View File

@ -1,6 +1,6 @@
import { IShopifyMediaService } from "../interfaces/IShopifyMediaService"
import { IShop } from "../interfaces/IShop"
import { formatGqlForJSON } from "../shopifyApi"
import { formatGqlForJSON, buildGqlQuery } from "../shopifyApi"
export class ShopifyMediaService implements IShopifyMediaService {
private shop: IShop
@ -29,10 +29,7 @@ export class ShopifyMediaService implements IShopifyMediaService {
}
`
const variables = { input }
const payload = {
query: formatGqlForJSON(query),
variables: variables
}
const payload = buildGqlQuery(query, variables)
const response = this.shop.shopifyGraphQLAPI(payload)
return response.content.data.stagedUploadsCreate
}
@ -62,10 +59,7 @@ export class ShopifyMediaService implements IShopifyMediaService {
productId,
media
}
const payload = {
query: formatGqlForJSON(query),
variables: variables
}
const payload = buildGqlQuery(query, variables)
const response = this.shop.shopifyGraphQLAPI(payload)
return response.content.data.productCreateMedia
}
@ -91,33 +85,37 @@ export class ShopifyMediaService implements IShopifyMediaService {
}
`
const variables = { productId }
const payload = {
query: formatGqlForJSON(query),
variables: variables
}
const payload = buildGqlQuery(query, variables)
const response = this.shop.shopifyGraphQLAPI(payload)
if (!response.content.data.product) return []
if (!response || !response.content || !response.content.data || !response.content.data.product) {
console.error("getProductMedia: Invalid response or product not found. Raw Response:", JSON.stringify(response));
throw new Error(`Product not found or access denied for ID: ${productId}. See Logs for details.`);
}
return response.content.data.product.media.edges.map((edge: any) => edge.node)
}
productDeleteMedia(productId: string, mediaId: string): any {
const query = /* GraphQL */ `
mutation productDeleteMedia($mediaId: ID!, $productId: ID!) {
productDeleteMedia(mediaId: $mediaId, productId: $productId) {
deletedMediaId
userErrors {
mutation productDeleteMedia($mediaIds: [ID!]!, $productId: ID!) {
productDeleteMedia(mediaIds: $mediaIds, productId: $productId) {
deletedMediaIds
mediaUserErrors {
field
message
}
}
}
`
const variables = { productId, mediaId }
const payload = {
query: formatGqlForJSON(query),
variables: variables
}
const variables = { productId, mediaIds: [mediaId] }
const payload = buildGqlQuery(query, variables)
const response = this.shop.shopifyGraphQLAPI(payload)
if (!response || !response.content || !response.content.data) {
console.error("productDeleteMedia failed. Response:", JSON.stringify(response))
if (response && response.content && response.content.errors) {
console.error("GraphQL Errors:", JSON.stringify(response.content.errors))
}
throw new Error(`Shopify API failed for productDeleteMedia: ${response ? 'Invalid Response' : 'No Response'}`)
}
return response.content.data.productDeleteMedia
}
@ -137,11 +135,13 @@ export class ShopifyMediaService implements IShopifyMediaService {
}
`
const variables = { id: productId, moves }
const payload = {
query: formatGqlForJSON(query),
variables: variables
}
const payload = buildGqlQuery(query, variables)
const response = this.shop.shopifyGraphQLAPI(payload)
return response.content.data.productReorderMedia
return response.content.data.productReorderMedia
}
getShopDomain(): string {
return this.shop.getShopDomain()
}
}