feat(syndication): implement channelAdapterService with Shopify GraphQL mutation generator and previewPayload endpoint

This commit is contained in:
Inamul-hasan-tec
2026-08-12 15:02:17 +05:30
parent c5821f8015
commit 66a55deb0d
4 changed files with 120 additions and 2 deletions
@@ -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
});
}