Product/sku deletion fixes
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { WebflowClient } from "@blade-and-brawn/commerce";
|
||||
import { env } from "../util";
|
||||
|
||||
const webflow = new WebflowClient({
|
||||
siteId: env.WEBFLOW_SITE_ID,
|
||||
collectionIds: {
|
||||
products: env.WEBFLOW_COLLECTION_PRODUCTS_ID,
|
||||
skus: env.WEBFLOW_COLLECTION_SKUS_ID
|
||||
},
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
const collections = await webflow.Collections.list();
|
||||
console.log(collections);
|
||||
@@ -40,7 +40,10 @@ try {
|
||||
console.log("Registering webflow webhooks...");
|
||||
const webflow = new WebflowClient({
|
||||
siteId: env.WEBFLOW_SITE_ID,
|
||||
collectionsId: env.WEBFLOW_COLLECTION_ID,
|
||||
collectionIds: {
|
||||
products: env.WEBFLOW_COLLECTION_PRODUCTS_ID,
|
||||
skus: env.WEBFLOW_COLLECTION_SKUS_ID
|
||||
},
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
@@ -33,7 +33,10 @@ const printful = new PrintfulClient({
|
||||
|
||||
const webflow = new WebflowClient({
|
||||
siteId: env.WEBFLOW_SITE_ID,
|
||||
collectionsId: env.WEBFLOW_COLLECTION_ID,
|
||||
collectionIds: {
|
||||
products: env.WEBFLOW_COLLECTION_PRODUCTS_ID,
|
||||
skus: env.WEBFLOW_COLLECTION_SKUS_ID
|
||||
},
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
@@ -16,7 +16,10 @@ export class CommerceService {
|
||||
});
|
||||
readonly Webflow = new WebflowClient({
|
||||
siteId: env.WEBFLOW_SITE_ID,
|
||||
collectionsId: env.WEBFLOW_COLLECTION_ID,
|
||||
collectionIds: {
|
||||
products: env.WEBFLOW_COLLECTION_PRODUCTS_ID,
|
||||
skus: env.WEBFLOW_COLLECTION_SKUS_ID
|
||||
},
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@ export const env = {
|
||||
PRINTFUL_AUTH: requireEnv("PRINTFUL_AUTH"),
|
||||
PRINTFUL_STORE_ID: requireEnv("PRINTFUL_STORE_ID"),
|
||||
WEBFLOW_SITE_ID: requireEnv("WEBFLOW_SITE_ID"),
|
||||
WEBFLOW_COLLECTION_ID: requireEnv("WEBFLOW_COLLECTION_ID"),
|
||||
WEBFLOW_COLLECTION_SKUS_ID: requireEnv("WEBFLOW_COLLECTION_SKUS_ID"),
|
||||
WEBFLOW_COLLECTION_PRODUCTS_ID: requireEnv("WEBFLOW_COLLECTION_PRODUCTS_ID"),
|
||||
WEBFLOW_AUTH: requireEnv("WEBFLOW_AUTH"),
|
||||
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"),
|
||||
AUTH_SECRET: requireEnv("AUTH_SECRET"),
|
||||
|
||||
@@ -5,7 +5,10 @@ import type { PageServerLoad } from "./$types";
|
||||
const printful = new PrintfulClient({ token: env.PRINTFUL_AUTH, storeId: env.PRINTFUL_STORE_ID });
|
||||
const webflow = new WebflowClient({
|
||||
siteId: env.WEBFLOW_SITE_ID,
|
||||
collectionsId: env.WEBFLOW_COLLECTION_ID,
|
||||
collectionIds: {
|
||||
products: env.WEBFLOW_COLLECTION_PRODUCTS_ID,
|
||||
skus: env.WEBFLOW_COLLECTION_SKUS_ID,
|
||||
},
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
@@ -145,6 +145,27 @@ export class ProductSyncer {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove webflow SKUs whose color+size no longer corresponds to any current printful variant.
|
||||
const currentPKeys = new Set<string>(
|
||||
pMetaProduct.entries.flatMap(({ colorGroup, product: pProduct }) =>
|
||||
pProduct.sync_variants.map((pVariant) => `${colorGroup ?? pVariant.color}::${pVariant.size}`)
|
||||
)
|
||||
);
|
||||
const orphanedWSkus = wProduct.skus.filter((existingWSku) =>
|
||||
!currentPKeys.has(`${existingWSku.fieldData["sku-values"]?.["color"]}::${existingWSku.fieldData["sku-values"]?.["size"]}`)
|
||||
);
|
||||
for (const orphanedWSku of orphanedWSkus) {
|
||||
this.log.info(
|
||||
{
|
||||
wSkuId: orphanedWSku.id,
|
||||
color: orphanedWSku.fieldData["sku-values"]?.["color"],
|
||||
size: orphanedWSku.fieldData["sku-values"]?.["size"],
|
||||
},
|
||||
"removing webflow SKU no longer present in printful",
|
||||
);
|
||||
await this.Webflow.Products.Skus.remove(orphanedWSku.id);
|
||||
}
|
||||
} else {
|
||||
this.log.info("existing webflow product not found, creating it");
|
||||
|
||||
|
||||
@@ -431,6 +431,17 @@ export namespace Webflow {
|
||||
};
|
||||
}
|
||||
|
||||
export namespace Collections {
|
||||
export type Collection = {
|
||||
id: string,
|
||||
displayName: string,
|
||||
singularName: string,
|
||||
slug: string,
|
||||
createdOn: string,
|
||||
lastUpdated: string
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Webhook {
|
||||
interface MetaData<E extends Event, D> {
|
||||
triggerType: E;
|
||||
|
||||
@@ -8,7 +8,10 @@ const parsePayload = (res: Response): Promise<unknown> =>
|
||||
|
||||
type ClientOptions = {
|
||||
siteId: string;
|
||||
collectionsId: string;
|
||||
collectionIds: {
|
||||
products: string,
|
||||
skus: string
|
||||
},
|
||||
token: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
@@ -16,7 +19,8 @@ type ClientOptions = {
|
||||
type Credentials = {
|
||||
apiBaseUrl: string;
|
||||
apiSitesUrl: string;
|
||||
apiCollectionsUrl: string;
|
||||
apiCollectionProductsUrl: string,
|
||||
apiCollectionSkusUrl: string,
|
||||
authHeader: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -81,6 +85,22 @@ class SkusClient {
|
||||
if (res.status === 429) throw new WebflowRateLimitError(res.headers.get("Retry-After"), await parsePayload(res));
|
||||
if (!res.ok) throw new WebflowError("Failed to update Webflow product SKU", res.status, await parsePayload(res));
|
||||
}
|
||||
|
||||
async remove(wSkuId: string): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${this.creds.apiCollectionSkusUrl}/items/${wSkuId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...this.creds.authHeader,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
if (res.status === 429) throw new WebflowRateLimitError(res.headers.get("Retry-After"), await parsePayload(res));
|
||||
if (!res.ok) throw new WebflowError("Failed to remove Webflow product SKU", res.status, await parsePayload(res));
|
||||
}
|
||||
}
|
||||
|
||||
class ProductsClient {
|
||||
@@ -154,7 +174,7 @@ class ProductsClient {
|
||||
|
||||
async remove(wProductId: string): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${this.creds.apiCollectionsUrl}/items/${wProductId}`,
|
||||
`${this.creds.apiCollectionProductsUrl}/items/${wProductId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -289,6 +309,23 @@ class WebhooksClient {
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionsClient {
|
||||
constructor(private creds: Credentials) { }
|
||||
|
||||
async list(): Promise<Webflow.Collections.Collection[]> {
|
||||
const allCollections: Webflow.Collections.Collection[] = [];
|
||||
const res = await fetch(`${this.creds.apiSitesUrl}/collections`, {
|
||||
method: "GET",
|
||||
headers: this.creds.authHeader,
|
||||
});
|
||||
if (res.status === 429) throw new WebflowRateLimitError(res.headers.get("Retry-After"), await parsePayload(res));
|
||||
if (!res.ok) throw new WebflowError("Failed to list Webflow collections", res.status, await parsePayload(res));
|
||||
const payload = (await res.json()) as Webflow.MetaDataMulti<"collections", Webflow.Collections.Collection>;
|
||||
allCollections.push(...payload.collections);
|
||||
return allCollections;
|
||||
}
|
||||
}
|
||||
|
||||
class UtilClient {
|
||||
constructor(private webhookSecret: string) { }
|
||||
|
||||
@@ -325,18 +362,21 @@ export class WebflowClient {
|
||||
readonly Products: ProductsClient;
|
||||
readonly Orders: OrdersClient;
|
||||
readonly Webhooks: WebhooksClient;
|
||||
readonly Collections: CollectionsClient;
|
||||
readonly Util: UtilClient;
|
||||
|
||||
constructor(options: ClientOptions) {
|
||||
const creds: Credentials = {
|
||||
apiBaseUrl: `https://api.webflow.com/v2`,
|
||||
apiSitesUrl: `https://api.webflow.com/v2/sites/${options.siteId}`,
|
||||
apiCollectionsUrl: `https://api.webflow.com/v2/collections/${options.collectionsId}`,
|
||||
apiCollectionProductsUrl: `https://api.webflow.com/v2/collections/${options.collectionIds.products}`,
|
||||
apiCollectionSkusUrl: `https://api.webflow.com/v2/collections/${options.collectionIds.skus}`,
|
||||
authHeader: { Authorization: `bearer ${options.token}` },
|
||||
};
|
||||
this.Products = new ProductsClient(creds);
|
||||
this.Orders = new OrdersClient(creds);
|
||||
this.Webhooks = new WebhooksClient(creds);
|
||||
this.Collections = new CollectionsClient(creds);
|
||||
this.Util = new UtilClient(options.webhookSecret);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user