
* Add required validation for forms * Add logs * [skip-ci] handle form errors * Improve notifications after form created * add notification when bulk sync finishes * Add skeletons * Validation for channel connection form * cr fixes
187 lines
5.2 KiB
TypeScript
187 lines
5.2 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";
|
|
import { FieldsMapper } from "../fields-mapper";
|
|
|
|
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 fields = FieldsMapper.mapProductVariantToConfigurationFields({
|
|
variant,
|
|
configMapping: configuration.productVariantFieldsMapping,
|
|
});
|
|
|
|
/**
|
|
* Dato requires JSON to be stringified first so overwrite this single fields
|
|
*/
|
|
fields[configuration.productVariantFieldsMapping.channels] = JSON.stringify(
|
|
variant.channelListings
|
|
);
|
|
|
|
return {
|
|
item_type: { type: "item_type", id: configuration.itemType },
|
|
...fields,
|
|
};
|
|
}
|
|
|
|
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");
|
|
}
|
|
});
|
|
}
|
|
}
|