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...");
|
console.log("Registering webflow webhooks...");
|
||||||
const webflow = new WebflowClient({
|
const webflow = new WebflowClient({
|
||||||
siteId: env.WEBFLOW_SITE_ID,
|
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,
|
token: env.WEBFLOW_AUTH,
|
||||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ const printful = new PrintfulClient({
|
|||||||
|
|
||||||
const webflow = new WebflowClient({
|
const webflow = new WebflowClient({
|
||||||
siteId: env.WEBFLOW_SITE_ID,
|
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,
|
token: env.WEBFLOW_AUTH,
|
||||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ export class CommerceService {
|
|||||||
});
|
});
|
||||||
readonly Webflow = new WebflowClient({
|
readonly Webflow = new WebflowClient({
|
||||||
siteId: env.WEBFLOW_SITE_ID,
|
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,
|
token: env.WEBFLOW_AUTH,
|
||||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export const env = {
|
|||||||
PRINTFUL_AUTH: requireEnv("PRINTFUL_AUTH"),
|
PRINTFUL_AUTH: requireEnv("PRINTFUL_AUTH"),
|
||||||
PRINTFUL_STORE_ID: requireEnv("PRINTFUL_STORE_ID"),
|
PRINTFUL_STORE_ID: requireEnv("PRINTFUL_STORE_ID"),
|
||||||
WEBFLOW_SITE_ID: requireEnv("WEBFLOW_SITE_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_AUTH: requireEnv("WEBFLOW_AUTH"),
|
||||||
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"),
|
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"),
|
||||||
AUTH_SECRET: requireEnv("AUTH_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 printful = new PrintfulClient({ token: env.PRINTFUL_AUTH, storeId: env.PRINTFUL_STORE_ID });
|
||||||
const webflow = new WebflowClient({
|
const webflow = new WebflowClient({
|
||||||
siteId: env.WEBFLOW_SITE_ID,
|
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,
|
token: env.WEBFLOW_AUTH,
|
||||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
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 {
|
} else {
|
||||||
this.log.info("existing webflow product not found, creating it");
|
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 {
|
export namespace Webhook {
|
||||||
interface MetaData<E extends Event, D> {
|
interface MetaData<E extends Event, D> {
|
||||||
triggerType: E;
|
triggerType: E;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ const parsePayload = (res: Response): Promise<unknown> =>
|
|||||||
|
|
||||||
type ClientOptions = {
|
type ClientOptions = {
|
||||||
siteId: string;
|
siteId: string;
|
||||||
collectionsId: string;
|
collectionIds: {
|
||||||
|
products: string,
|
||||||
|
skus: string
|
||||||
|
},
|
||||||
token: string;
|
token: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
};
|
};
|
||||||
@@ -16,7 +19,8 @@ type ClientOptions = {
|
|||||||
type Credentials = {
|
type Credentials = {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
apiSitesUrl: string;
|
apiSitesUrl: string;
|
||||||
apiCollectionsUrl: string;
|
apiCollectionProductsUrl: string,
|
||||||
|
apiCollectionSkusUrl: string,
|
||||||
authHeader: Record<string, 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.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));
|
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 {
|
class ProductsClient {
|
||||||
@@ -154,7 +174,7 @@ class ProductsClient {
|
|||||||
|
|
||||||
async remove(wProductId: string): Promise<void> {
|
async remove(wProductId: string): Promise<void> {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${this.creds.apiCollectionsUrl}/items/${wProductId}`,
|
`${this.creds.apiCollectionProductsUrl}/items/${wProductId}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
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 {
|
class UtilClient {
|
||||||
constructor(private webhookSecret: string) { }
|
constructor(private webhookSecret: string) { }
|
||||||
|
|
||||||
@@ -325,18 +362,21 @@ export class WebflowClient {
|
|||||||
readonly Products: ProductsClient;
|
readonly Products: ProductsClient;
|
||||||
readonly Orders: OrdersClient;
|
readonly Orders: OrdersClient;
|
||||||
readonly Webhooks: WebhooksClient;
|
readonly Webhooks: WebhooksClient;
|
||||||
|
readonly Collections: CollectionsClient;
|
||||||
readonly Util: UtilClient;
|
readonly Util: UtilClient;
|
||||||
|
|
||||||
constructor(options: ClientOptions) {
|
constructor(options: ClientOptions) {
|
||||||
const creds: Credentials = {
|
const creds: Credentials = {
|
||||||
apiBaseUrl: `https://api.webflow.com/v2`,
|
apiBaseUrl: `https://api.webflow.com/v2`,
|
||||||
apiSitesUrl: `https://api.webflow.com/v2/sites/${options.siteId}`,
|
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}` },
|
authHeader: { Authorization: `bearer ${options.token}` },
|
||||||
};
|
};
|
||||||
this.Products = new ProductsClient(creds);
|
this.Products = new ProductsClient(creds);
|
||||||
this.Orders = new OrdersClient(creds);
|
this.Orders = new OrdersClient(creds);
|
||||||
this.Webhooks = new WebhooksClient(creds);
|
this.Webhooks = new WebhooksClient(creds);
|
||||||
|
this.Collections = new CollectionsClient(creds);
|
||||||
this.Util = new UtilClient(options.webhookSecret);
|
this.Util = new UtilClient(options.webhookSecret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user