@getverdict/n8n-nodes-shopify-partner-relationship-events-trigger
Version:
n8n trigger node that polls Shopify Partner RelationshipEvents via GraphQL
186 lines (185 loc) • 9.24 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShopifyPartnerRelationshipEventsTrigger = void 0;
const n8n_workflow_1 = require("n8n-workflow");
function buildDefaultQuery() {
return `query AppEvents($appId: ID!, $first: Int!, $after: String, $types: [AppEventTypes!]) {
app(id: $appId) {
events(first: $first, after: $after, types: $types) {
edges {
cursor
node { __typename occurredAt shop { avatarUrl id myshopifyDomain name } }
}
pageInfo { hasNextPage }
}
}
}`;
}
class ShopifyPartnerRelationshipEventsTrigger {
constructor() {
this.description = {
displayName: 'Shopify Partner: Relationship Events (Trigger)',
name: 'shopifyPartnerRelationshipEventsTrigger',
icon: 'fa:bolt',
group: ['trigger'],
version: 2,
description: 'Poll Shopify Partner API RelationshipEvents and emit new events',
defaults: { name: 'Shopify Partner: Relationship Events' },
inputs: [],
outputs: ["main" /* NodeConnectionType.Main */],
credentials: [{ name: 'shopifyPartnerApi', required: true }],
polling: true,
properties: [
{
displayName: 'App ID (GID)',
name: 'appId',
type: 'string',
default: '',
required: true,
description: 'The App GID, e.g. gid://partners/App/123456',
},
{
displayName: 'API Version',
name: 'apiVersion',
type: 'string',
default: '2025-07',
description: 'Shopify Partner API version (e.g. 2025-07)',
},
{
displayName: 'Page Size',
name: 'pageSize',
type: 'number',
typeOptions: { minValue: 1, maxValue: 250 },
default: 50,
description: 'Number of events to request per poll page',
},
{
displayName: 'Emit Existing Events On First Run',
name: 'emitExistingOnStart',
type: 'boolean',
default: false,
description: 'If disabled, initializes cursor without emitting existing events on first run',
},
{
displayName: 'Event Types Filter',
name: 'eventTypes',
type: 'multiOptions',
default: [],
description: 'Filter by event types server-side; leave empty for all',
options: [
{ name: 'RELATIONSHIP_INSTALLED', value: 'RELATIONSHIP_INSTALLED' },
{ name: 'RELATIONSHIP_UNINSTALLED', value: 'RELATIONSHIP_UNINSTALLED' },
{ name: 'RELATIONSHIP_REACTIVATED', value: 'RELATIONSHIP_REACTIVATED' },
{ name: 'RELATIONSHIP_DEACTIVATED', value: 'RELATIONSHIP_DEACTIVATED' },
{ name: 'ONE_TIME_CHARGE_ACCEPTED', value: 'ONE_TIME_CHARGE_ACCEPTED' },
{ name: 'ONE_TIME_CHARGE_ACTIVATED', value: 'ONE_TIME_CHARGE_ACTIVATED' },
{ name: 'ONE_TIME_CHARGE_DECLINED', value: 'ONE_TIME_CHARGE_DECLINED' },
{ name: 'ONE_TIME_CHARGE_EXPIRED', value: 'ONE_TIME_CHARGE_EXPIRED' },
{ name: 'SUBSCRIPTION_CHARGE_ACCEPTED', value: 'SUBSCRIPTION_CHARGE_ACCEPTED' },
{ name: 'SUBSCRIPTION_CHARGE_ACTIVATED', value: 'SUBSCRIPTION_CHARGE_ACTIVATED' },
{ name: 'SUBSCRIPTION_CHARGE_CANCELED', value: 'SUBSCRIPTION_CHARGE_CANCELED' },
{ name: 'SUBSCRIPTION_CHARGE_DECLINED', value: 'SUBSCRIPTION_CHARGE_DECLINED' },
{ name: 'SUBSCRIPTION_CHARGE_EXPIRED', value: 'SUBSCRIPTION_CHARGE_EXPIRED' },
{ name: 'SUBSCRIPTION_CHARGE_FROZEN', value: 'SUBSCRIPTION_CHARGE_FROZEN' },
{ name: 'SUBSCRIPTION_CHARGE_UNFROZEN', value: 'SUBSCRIPTION_CHARGE_UNFROZEN' },
{ name: 'SUBSCRIPTION_CAPPED_AMOUNT_UPDATED', value: 'SUBSCRIPTION_CAPPED_AMOUNT_UPDATED' },
{ name: 'SUBSCRIPTION_APPROACHING_CAPPED_AMOUNT', value: 'SUBSCRIPTION_APPROACHING_CAPPED_AMOUNT' },
{ name: 'CREDIT_APPLIED', value: 'CREDIT_APPLIED' },
{ name: 'CREDIT_FAILED', value: 'CREDIT_FAILED' },
{ name: 'CREDIT_PENDING', value: 'CREDIT_PENDING' },
{ name: 'USAGE_CHARGE_APPLIED', value: 'USAGE_CHARGE_APPLIED' },
],
},
],
};
}
async poll() {
const credentials = await this.getCredentials('shopifyPartnerApi');
const orgRaw = (credentials?.organizationId || '').trim();
if (!orgRaw) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Organization ID is required (set it in credentials or provide an override).');
}
const organizationId = orgRaw.match(/^gid:\/\/partners\/Organization\//)
? orgRaw
: `gid://partners/Organization/${orgRaw}`;
const creds = await this.getCredentials('shopifyPartnerApi');
const credApiVersion = (creds?.apiVersion || '').trim();
const useJsonSuffix = creds?.useJsonSuffix !== false; // default true
const apiVersion = (this.getNodeParameter('apiVersion', 0) || credApiVersion || '2025-07').trim();
// Poll scheduling is handled by n8n
const pageSize = this.getNodeParameter('pageSize', 0) || 50;
const emitExistingOnStart = this.getNodeParameter('emitExistingOnStart', 0);
const eventTypes = this.getNodeParameter('eventTypes', 0) || [];
const customQuery = buildDefaultQuery();
const state = this.getWorkflowStaticData('node');
let lastCursor = state.lastCursor || '';
let initialized = Boolean(state.initialized);
const orgNumericId = organizationId.split('/').pop();
const endpoint = `https://partners.shopify.com/${orgNumericId}/api/${apiVersion}/graphql${useJsonSuffix ? '.json' : ''}`;
const fetchPage = async (afterCursor) => {
const appIdInput = this.getNodeParameter('appId', 0).trim();
const appId = appIdInput.match(/^gid:\/\/partners\/App\//)
? appIdInput
: `gid://partners/App/${appIdInput}`;
const baseVariables = {
appId,
first: pageSize,
};
if (afterCursor) {
baseVariables.after = afterCursor;
}
if (eventTypes.length > 0) {
baseVariables.types = eventTypes;
}
const variables = baseVariables;
const requestOptions = {
method: 'POST',
url: endpoint,
body: { query: customQuery, variables },
json: true,
};
const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'shopifyPartnerApi', requestOptions));
return response;
};
const extractConnection = (response) => {
// Surface GraphQL errors
const gqlErrors = response.errors;
if (Array.isArray(gqlErrors) && gqlErrors.length > 0) {
throw new Error(JSON.stringify(gqlErrors));
}
const data = response.data || {};
const app = data.app || {};
const conn = app.events || {};
return conn;
};
try {
const response = await fetchPage(lastCursor || undefined);
const conn = extractConnection(response);
const edges = (conn.edges || []);
const pageInfo = conn.pageInfo || {};
const endCursor = edges.length > 0 ? edges[edges.length - 1].cursor : lastCursor;
if (!initialized && !emitExistingOnStart) {
// Initialize cursor without emitting existing
state.lastCursor = endCursor || lastCursor;
state.initialized = true;
return [];
}
const items = [];
for (const edge of edges) {
const cursor = edge.cursor || undefined;
const node = edge.node || {};
items.push({ json: { cursor, event: node } });
}
if (endCursor) {
state.lastCursor = endCursor;
}
return items.length > 0 ? [items] : null;
}
catch (error) {
const errMsg = error?.message || 'Unknown error during Partner API poll';
return [[{ json: { error: true, message: errMsg } }]];
}
}
}
exports.ShopifyPartnerRelationshipEventsTrigger = ShopifyPartnerRelationshipEventsTrigger;
exports.default = ShopifyPartnerRelationshipEventsTrigger;