feat(syndication): implement channelAdapterService with Shopify GraphQL mutation generator and previewPayload endpoint
This commit is contained in:
@@ -112,6 +112,15 @@ export class ChannelController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async previewPayload(req, res, next) {
|
||||
try {
|
||||
const data = await syndicationService.previewPayload(req.params.id, req.query.productId);
|
||||
return res.status(200).json({ success: true, data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChannelController();
|
||||
|
||||
@@ -182,4 +182,11 @@ router.get(
|
||||
controller.getJobById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/preview',
|
||||
authenticate,
|
||||
authorize(['settings.integrations']),
|
||||
controller.previewPayload
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
export class ChannelAdapterService {
|
||||
/**
|
||||
* Format transformed payload into a Shopify GraphQL productCreate mutation
|
||||
*/
|
||||
formatShopifyGraphQL(transformedPayload) {
|
||||
return {
|
||||
query: `
|
||||
mutation productCreate($input: ProductInput!) {
|
||||
productCreate(input: $input) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
handle
|
||||
status
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
title: transformedPayload.title || transformedPayload.name || 'Untitled Product',
|
||||
bodyHtml: transformedPayload.body_html || transformedPayload.description || '',
|
||||
vendor: transformedPayload.vendor || transformedPayload.brand || 'Generic',
|
||||
productType: transformedPayload.product_type || 'General',
|
||||
status: transformedPayload.published_status === 'published' ? 'ACTIVE' : 'DRAFT',
|
||||
variants: [
|
||||
{
|
||||
sku: transformedPayload.variant_sku || transformedPayload.code || 'SKU-DEFAULT',
|
||||
price: String(transformedPayload.price || '0.00'),
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format payload for custom Webhook HTTP POST dispatches
|
||||
*/
|
||||
formatWebhookPayload(channel, transformedPayload) {
|
||||
return {
|
||||
event: 'product.syndicated',
|
||||
channel: {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
code: channel.code
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
data: transformedPayload
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChannelAdapterService();
|
||||
@@ -1,6 +1,7 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import channelAdapterService from './channelAdapter.service.js';
|
||||
|
||||
export class SyndicationService {
|
||||
applyTransformation(val, rule, defaultValue) {
|
||||
@@ -26,6 +27,49 @@ export class SyndicationService {
|
||||
}
|
||||
}
|
||||
|
||||
async previewPayload(channelId, productId) {
|
||||
const channel = await models.Channel.findByPk(channelId);
|
||||
if (!channel) {
|
||||
throw new ApiError(404, 'Channel not found');
|
||||
}
|
||||
|
||||
const mappings = await models.ChannelMapping.findAll({
|
||||
where: { channel_id: channelId }
|
||||
});
|
||||
|
||||
let product = null;
|
||||
if (productId) {
|
||||
product = await models.Product.findByPk(productId);
|
||||
} else {
|
||||
product = await models.Product.findOne();
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new ApiError(404, 'No product available for payload transformation preview');
|
||||
}
|
||||
|
||||
const transformed = {};
|
||||
for (const mapItem of mappings) {
|
||||
const rawVal = product[mapItem.pim_attribute_code];
|
||||
transformed[mapItem.channel_field_code] = this.applyTransformation(
|
||||
rawVal,
|
||||
mapItem.transformation_rule,
|
||||
mapItem.default_value
|
||||
);
|
||||
}
|
||||
|
||||
const formattedAdapterPayload = (channel.code === 'shopify' || channel.channelType === 'ecommerce')
|
||||
? channelAdapterService.formatShopifyGraphQL(transformed)
|
||||
: channelAdapterService.formatWebhookPayload(channel, transformed);
|
||||
|
||||
return {
|
||||
channel: { id: channel.id, name: channel.name, code: channel.code },
|
||||
pimProductRaw: product,
|
||||
transformedFields: transformed,
|
||||
adapterOutput: formattedAdapterPayload
|
||||
};
|
||||
}
|
||||
|
||||
async triggerSyndication(channelId, userContext = {}) {
|
||||
const channel = await models.Channel.findByPk(channelId);
|
||||
if (!channel) {
|
||||
@@ -73,7 +117,7 @@ export class SyndicationService {
|
||||
if (mapItem.is_required && (rawVal === null || rawVal === undefined || rawVal === '')) {
|
||||
errorLogs.push({
|
||||
productId: prod.id,
|
||||
sku: prod.sku,
|
||||
sku: prod.code || prod.sku,
|
||||
error: `Required attribute "${mapItem.pim_attribute_code}" is missing or null`
|
||||
});
|
||||
hasError = true;
|
||||
@@ -95,7 +139,7 @@ export class SyndicationService {
|
||||
failedCount++;
|
||||
errorLogs.push({
|
||||
productId: prod.id,
|
||||
sku: prod.sku,
|
||||
sku: prod.code || prod.sku,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user