
* initial setup * GRaphql setup * [skip ci] * Manifest and register endpoints * Add config schemas * contentful client * contentful client * [skip ci] trpc setup * metadata manager and contentful router * Configuration config * contentful config - adding providers * add provider page * wip contentful form * contentful form * list providrs * edit contentful form * [skip ci] * [skip ci] replace contentful sdk * replace contentful lib * Delete contetnful provider feature * variant created webhook draft * channel provider connection model * Channel connections ui * adding new connection * connections ui (adding) * [skip ci] wip edit conn * removing connection * rebuild modal * refactor providers * implement update product webhook * add deleting product * [skip ci] wip * refactor contentful router * refactor wip * refactor config * webhooks processor * webhook delegations * bulk sync section * bulk sync page * gql for imports * [skip ci] bulk import contentful * temp variant sync list with rate limiters * wip * wip * wip * new frontend for uploading * update zod * print config keys * wip * [skip ci] datocms init * dato add provdier page * dato form skeleton * dato display content type select * full dato form * ButtonsBox extraction * edit dato config form * update product in dato * [skip ci] * extract bulk sync processor * dato bulk update * [skip ci] product updated webhook * product webhook * crud operations router * update cruds * refactor webhook operations * refactors * refactors * helper texts * [skip ci] deps * Init * fix macaw icon * unify app skd * unify nextjs * strapi setup * fix strapi types * strapi upload product * strapi delete product * strapi product updated webhook * processor for bulk sync strapi * shared add provider page * refactors * refactors * wrap providers into folder * refactors * refactors * refactors * pnpm lock * add logs * name configuration mapping name * form configurable side notes * remove commentns * wip providers resolver working * central config for providers resolving * tests wip * remove vscode condig * cleanup * provider meta extract * some tests for contentufl * contentful client test * more tests for contentful * strapi helper texts * builderio setup * builderio form * builderio client draft * validate connection modal * Add sending product to builderio * rename builder field * add public api key for read access * update products * Builder.io - bulk sync * Fix manifest * cr fixes * Make strapi to work with multiple products * Github actions
181 lines
5.1 KiB
TypeScript
181 lines
5.1 KiB
TypeScript
import { buildClient, Client, SimpleSchemaTypes, ApiError } from "@datocms/cma-client-browser";
|
|
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
|
|
import { createLogger } from "@saleor/apps-shared";
|
|
import { z } from "zod";
|
|
|
|
import * as Sentry from "@sentry/nextjs";
|
|
import { DatocmsProviderConfig } from "@/modules/configuration/schemas/datocms-provider.schema";
|
|
|
|
type Context = {
|
|
configuration: DatocmsProviderConfig.FullShape;
|
|
variant: WebhookProductVariantFragment;
|
|
};
|
|
|
|
/*
|
|
* todo error handling
|
|
*/
|
|
export class DatoCMSClient {
|
|
private client: Client;
|
|
private logger = createLogger({ name: "DatoCMSClient" });
|
|
|
|
constructor(opts: { apiToken: string }) {
|
|
this.client = buildClient({ apiToken: opts.apiToken });
|
|
}
|
|
|
|
getContentTypes() {
|
|
this.logger.trace("Trying to get content types");
|
|
|
|
return this.client.itemTypes.list();
|
|
}
|
|
|
|
getFieldsForContentType({ itemTypeID }: { itemTypeID: string }) {
|
|
this.logger.trace("Trying to get fields for a content type");
|
|
|
|
return this.client.fields.list({ type: "item_type", id: itemTypeID });
|
|
}
|
|
|
|
private getItemBySaleorVariantId({
|
|
variantIdFieldName: variantFieldName,
|
|
variantID,
|
|
contentType,
|
|
}: {
|
|
variantIdFieldName: string;
|
|
variantID: string;
|
|
contentType: string;
|
|
}) {
|
|
this.logger.trace("Trying to fetch item by Saleor variant ID", { variantID: variantID });
|
|
|
|
return this.client.items.list({
|
|
filter: {
|
|
type: contentType,
|
|
fields: {
|
|
[variantFieldName]: {
|
|
eq: variantID,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
private mapVariantToDatoCMSFields({
|
|
configuration,
|
|
variant,
|
|
}: Context): SimpleSchemaTypes.ItemCreateSchema {
|
|
const fieldsMap = configuration.productVariantFieldsMapping;
|
|
|
|
return {
|
|
item_type: { type: "item_type", id: configuration.itemType },
|
|
[fieldsMap.variantName]: variant.name,
|
|
[fieldsMap.productId]: variant.product.id,
|
|
[fieldsMap.productName]: variant.product.name,
|
|
[fieldsMap.productSlug]: variant.product.slug,
|
|
[fieldsMap.variantId]: variant.id,
|
|
[fieldsMap.channels]: JSON.stringify(variant.channelListings),
|
|
};
|
|
}
|
|
|
|
async deleteProductVariant({ configuration, variant }: Context) {
|
|
this.logger.debug("Trying to delete product variant");
|
|
|
|
const remoteProducts = await this.getItemBySaleorVariantId({
|
|
variantIdFieldName: configuration.productVariantFieldsMapping.variantId,
|
|
variantID: variant.id,
|
|
contentType: configuration.itemType,
|
|
});
|
|
|
|
if (remoteProducts.length > 1) {
|
|
this.logger.warn(
|
|
"More than 1 variant with the same ID found in the CMS. Will remove all of them, but this should not happen if unique field was set"
|
|
);
|
|
}
|
|
|
|
if (remoteProducts.length === 0) {
|
|
this.logger.trace("No product found in Datocms, skipping deletion");
|
|
|
|
return;
|
|
}
|
|
|
|
return Promise.all(
|
|
remoteProducts.map((p) => {
|
|
return this.client.items.rawDestroy(p.id);
|
|
})
|
|
);
|
|
}
|
|
|
|
uploadProductVariant(context: Context) {
|
|
this.logger.debug("Trying to upload product variant");
|
|
|
|
return this.client.items.create(this.mapVariantToDatoCMSFields(context));
|
|
}
|
|
|
|
async updateProductVariant({ configuration, variant }: Context) {
|
|
const products = await this.getItemBySaleorVariantId({
|
|
variantIdFieldName: configuration.productVariantFieldsMapping.variantId,
|
|
variantID: variant.id,
|
|
contentType: configuration.itemType,
|
|
});
|
|
|
|
if (products.length > 1) {
|
|
this.logger.warn(
|
|
"Found more than one product variant with the same ID. Will update all of them, but this should not happen if unique field was set",
|
|
{
|
|
variantID: variant.id,
|
|
}
|
|
);
|
|
}
|
|
|
|
return Promise.all(
|
|
products.map((product) => {
|
|
this.logger.trace("Trying to update variant", { datoID: product.id });
|
|
|
|
return this.client.items.update(
|
|
product.id,
|
|
this.mapVariantToDatoCMSFields({
|
|
configuration,
|
|
variant,
|
|
})
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
upsertProduct({ configuration, variant }: Context) {
|
|
this.logger.debug("Trying to upsert product variant");
|
|
|
|
const DatoErrorBody = z.object({
|
|
data: z.array(
|
|
z.object({
|
|
validation: z.object({
|
|
attributes: z.object({
|
|
details: z.object({
|
|
code: z.string(),
|
|
}),
|
|
}),
|
|
}),
|
|
})
|
|
),
|
|
});
|
|
|
|
return this.uploadProductVariant({ configuration, variant }).catch((err: ApiError) => {
|
|
try {
|
|
const errorBody = DatoErrorBody.parse(err.response.body);
|
|
|
|
const isUniqueIdError = errorBody.data.find(
|
|
(d) => d.validation.attributes.details.code === "VALIDATION_UNIQUE"
|
|
);
|
|
|
|
if (isUniqueIdError) {
|
|
return this.updateProductVariant({ configuration, variant });
|
|
} else {
|
|
throw new Error(JSON.stringify(err.cause));
|
|
}
|
|
} catch (e) {
|
|
Sentry.captureException("Invalid error shape from DatoCMS", (c) => {
|
|
return c.setExtra("error", err);
|
|
});
|
|
|
|
throw new Error(err.humanMessage ?? "DatoCMS error - can upload product variant");
|
|
}
|
|
});
|
|
}
|
|
}
|