first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,140 @@
import { snakeCase } from 'change-case';
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IOAuth2Options,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
export async function shopifyApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
query: IDataObject = {},
uri?: string,
option: IDataObject = {},
): Promise<any> {
const authenticationMethod = this.getNodeParameter('authentication', 0, 'oAuth2') as string;
let credentials;
let credentialType = 'shopifyOAuth2Api';
if (authenticationMethod === 'apiKey') {
credentials = await this.getCredentials('shopifyApi');
credentialType = 'shopifyApi';
} else if (authenticationMethod === 'accessToken') {
credentials = await this.getCredentials('shopifyAccessTokenApi');
credentialType = 'shopifyAccessTokenApi';
} else {
credentials = await this.getCredentials('shopifyOAuth2Api');
}
const options: IRequestOptions = {
method,
qs: query,
uri: uri || `https://${credentials.shopSubdomain}.myshopify.com/admin/api/2024-07/${resource}`,
body,
json: true,
};
const oAuth2Options: IOAuth2Options = {
tokenType: 'Bearer',
keyToIncludeInAccessTokenHeader: 'X-Shopify-Access-Token',
};
if (authenticationMethod === 'apiKey') {
Object.assign(options, {
auth: { username: credentials.apiKey, password: credentials.password },
});
}
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
if (Object.keys(body as IDataObject).length === 0) {
delete options.body;
}
if (Object.keys(query).length === 0) {
delete options.qs;
}
// Only limit and fields are allowed for page_info links
// https://shopify.dev/docs/api/usage/pagination-rest#limitations-and-considerations
if (uri?.includes('page_info')) {
options.qs = {};
if (query.limit) {
options.qs.limit = query.limit;
}
if (query.fields) {
options.qs.fields = query.fields;
}
}
return await this.helpers.requestWithAuthentication.call(this, credentialType, options, {
oauth2: oAuth2Options,
});
}
export async function shopifyApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
resource: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
/*
When paginating some parameters
(e.g. product:getAll -> title ) cannot
be empty in the query string, so remove
all the empty ones before paginating.
*/
for (const field in query) {
if (query[field] === '') {
delete query[field];
}
}
let responseData;
let uri: string | undefined;
do {
responseData = await shopifyApiRequest.call(this, method, resource, body, query, uri, {
resolveWithFullResponse: true,
});
if (responseData.headers.link) {
uri = responseData.headers.link.split(';')[0].replace('<', '').replace('>', '');
}
returnData.push.apply(returnData, responseData.body[propertyName] as IDataObject[]);
} while (responseData.headers.link?.includes('rel="next"'));
return returnData;
}
export function keysToSnakeCase(elements: IDataObject[] | IDataObject): IDataObject[] {
if (elements === undefined) {
return [];
}
if (!Array.isArray(elements)) {
elements = [elements];
}
for (const element of elements) {
for (const key of Object.keys(element)) {
if (key !== snakeCase(key)) {
element[snakeCase(key)] = element[key];
delete element[key];
}
}
}
return elements;
}
@@ -0,0 +1,914 @@
import type { INodeProperties } from 'n8n-workflow';
export const orderOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['order'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create an order',
action: 'Create an order',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an order',
action: 'Delete an order',
},
{
name: 'Get',
value: 'get',
description: 'Get an order',
action: 'Get an order',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many orders',
action: 'Get many orders',
},
{
name: 'Update',
value: 'update',
description: 'Update an order',
action: 'Update an order',
},
],
default: 'create',
},
];
export const orderFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* order:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['order'],
},
},
default: {},
options: [
{
displayName: 'Billing Address',
name: 'billingAddressUi',
placeholder: 'Add Billing Address',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
name: 'billingAddressValues',
displayName: 'Billing Address',
values: [
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
},
{
displayName: 'Company',
name: 'company',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
{
displayName: 'Address Line 1',
name: 'address1',
type: 'string',
default: '',
},
{
displayName: 'Address Line 2',
name: 'address2',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'Province',
name: 'province',
type: 'string',
default: '',
},
{
displayName: 'Zip Code',
name: 'zip',
type: 'string',
default: '',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Discount Codes',
name: 'discountCodesUi',
placeholder: 'Add Discount Code',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'discountCodesValues',
displayName: 'Discount Code',
values: [
{
displayName: 'Amount',
name: 'amount',
type: 'string',
default: '',
description: "The amount that's deducted from the order total",
},
{
displayName: 'Code',
name: 'code',
type: 'string',
default: '',
description: 'When the associated discount application is of type code',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Fixed Amount',
value: 'fixedAmount',
description: "Applies amount as a unit of the store's currency",
},
{
name: 'Percentage',
value: 'percentage',
description: 'Applies a discount of amount as a percentage of the order total',
},
{
name: 'Shipping',
value: 'shipping',
description:
'Applies a free shipping discount on orders that have a shipping rate less than or equal to amount',
},
],
default: 'fixedAmount',
description: 'When the associated discount application is of type code',
},
],
},
],
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description: "The customer's email address",
},
{
displayName: 'Fulfillment Status',
name: 'fulfillmentStatus',
type: 'options',
options: [
{
name: 'Fulfilled',
value: 'fulfilled',
description: 'Every line item in the order has been fulfilled',
},
{
name: 'Null',
value: 'null',
description: 'None of the line items in the order have been fulfilled',
},
{
name: 'Partial',
value: 'partial',
description: 'At least one line item in the order has been fulfilled',
},
{
name: 'Restocked',
value: 'restocked',
description: 'Every line item in the order has been restocked and the order canceled',
},
],
default: '',
description: "The order's status in terms of fulfilled line items",
},
{
displayName: 'Inventory Behaviour',
name: 'inventoryBehaviour',
type: 'options',
options: [
{
name: 'Bypass',
value: 'bypass',
description: 'Do not claim inventory',
},
{
name: 'Decrement Ignoring Policy',
value: 'decrementIgnoringPolicy',
description: "Ignore the product's inventory policy and claim inventory",
},
{
name: 'Decrement Obeying Policy',
value: 'decrementObeyingPolicy',
description: "Follow the product's inventory policy and claim inventory, if possible",
},
],
default: 'bypass',
description: 'The behaviour to use when updating inventory',
},
{
displayName: 'Location Name or ID',
name: 'locationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLocations',
},
default: '',
description:
'The ID of the physical location where the order was processed. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
description: 'An optional note that a shop owner can attach to the order',
},
{
displayName: 'Send Fulfillment Receipt',
name: 'sendFulfillmentReceipt',
type: 'boolean',
default: false,
description: 'Whether to send a shipping confirmation to the customer',
},
{
displayName: 'Send Receipt',
name: 'sendReceipt',
type: 'boolean',
default: false,
description: 'Whether to send an order confirmation to the customer',
},
{
displayName: 'Shipping Address',
name: 'shippingAddressUi',
placeholder: 'Add Shipping',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
name: 'shippingAddressValues',
displayName: 'Shipping Address',
values: [
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
},
{
displayName: 'Company',
name: 'company',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
{
displayName: 'Address Line 1',
name: 'address1',
type: 'string',
default: '',
},
{
displayName: 'Address Line 2',
name: 'address2',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'Province',
name: 'province',
type: 'string',
default: '',
},
{
displayName: 'Zip Code',
name: 'zip',
type: 'string',
default: '',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Source Name',
name: 'sourceName',
type: 'string',
default: '',
description:
'Where the order originated. Can be set only during order creation, and is not writeable afterwards.',
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
description: 'Tags attached to the order, formatted as a string of comma-separated values',
},
{
displayName: 'Test',
name: 'test',
type: 'boolean',
default: false,
description: 'Whether this is a test order',
},
],
},
{
displayName: 'Line Items',
name: 'limeItemsUi',
placeholder: 'Add Line Item',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['order'],
operation: ['create'],
},
},
default: {},
options: [
{
displayName: 'Line Item',
name: 'lineItemValues',
values: [
{
displayName: 'Product Name or ID',
name: 'productId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProducts',
},
default: '',
description:
'The ID of the product that the line item belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Variant ID',
name: 'variantId',
type: 'string',
default: '',
description: 'The ID of the product variant',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'The title of the product',
},
{
displayName: 'Grams',
name: 'grams',
type: 'string',
default: '',
description: 'The weight of the item in grams',
},
{
displayName: 'Quantity',
name: 'quantity',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 1,
description: 'The number of items that were purchased',
},
{
displayName: 'Price',
name: 'price',
type: 'string',
default: '',
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* order:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Order ID',
name: 'orderId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['order'],
operation: ['delete'],
},
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* order:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Order ID',
name: 'orderId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['order'],
operation: ['get'],
},
},
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['get'],
resource: ['order'],
},
},
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description:
'Fields the order will return, formatted as a string of comma-separated values. By default all the fields are returned.',
},
],
},
/* -------------------------------------------------------------------------- */
/* order:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['order'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['order'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 250,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
resource: ['order'],
},
},
options: [
{
displayName: 'Attribution App ID',
name: 'attributionAppId',
type: 'string',
default: '',
description:
'Show orders attributed to a certain app, specified by the app ID. Set as current to show orders for the app currently consuming the API.',
},
{
displayName: 'Created At Min',
name: 'createdAtMin',
type: 'dateTime',
default: '',
description: 'Show orders created at or after date',
},
{
displayName: 'Created At Max',
name: 'createdAtMax',
type: 'dateTime',
default: '',
description: 'Show orders created at or before date',
},
{
displayName: 'Financial Status',
name: 'financialStatus',
type: 'options',
options: [
{
name: 'Any',
value: 'any',
description: 'Show orders of any financial status',
},
{
name: 'Authorized',
value: 'authorized',
description: 'Show only authorized orders',
},
{
name: 'Paid',
value: 'paid',
description: 'Show only paid orders',
},
{
name: 'Partially Paid',
value: 'partiallyPaid',
description: 'Show only partially paid orders',
},
{
name: 'Partially Refunded',
value: 'partiallyRefunded',
description: 'Show only partially refunded orders',
},
{
name: 'Pending',
value: 'pending',
description: 'Show only pending orders',
},
{
name: 'Refunded',
value: 'refunded',
description: 'Show only refunded orders',
},
{
name: 'Unpaid',
value: 'unpaid',
description: 'Show authorized and partially paid orders',
},
{
name: 'Voided',
value: 'voided',
description: 'Show only voided orders',
},
],
default: 'any',
description: 'Filter orders by their financial status',
},
{
displayName: 'Fulfillment Status',
name: 'fulfillmentStatus',
type: 'options',
options: [
{
name: 'Any',
value: 'any',
description: 'Show orders of any fulfillment status',
},
{
name: 'Partial',
value: 'partial',
description: 'Show partially shipped orders',
},
{
name: 'Shipped',
value: 'shipped',
description:
'Show orders that have been shipped. Returns orders with fulfillment_status of fulfilled.',
},
{
name: 'Unfulfilled',
value: 'unfulfilled',
description: 'Returns orders with fulfillment_status of null or partial',
},
{
name: 'Unshipped',
value: 'unshipped',
description:
'Show orders that have not yet been shipped. Returns orders with fulfillment_status of null.',
},
],
default: 'any',
description: 'Filter orders by their fulfillment status',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description:
'Fields the orders will return, formatted as a string of comma-separated values. By default all the fields are returned.',
},
{
displayName: 'IDs',
name: 'ids',
type: 'string',
default: '',
description: 'Retrieve only orders specified by a comma-separated list of order IDs',
},
{
displayName: 'Processed At Max',
name: 'processedAtMax',
type: 'dateTime',
default: '',
description: 'Show orders imported at or before date',
},
{
displayName: 'Processed At Min',
name: 'processedAtMin',
type: 'dateTime',
default: '',
description: 'Show orders imported at or after date',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Any',
value: 'any',
description: 'Show orders of any status, including archived orders',
},
{
name: 'Cancelled',
value: 'Cancelled',
description: 'Show only canceled orders',
},
{
name: 'Closed',
value: 'closed',
description: 'Show only closed orders',
},
{
name: 'Open',
value: 'open',
description: 'Show only open orders',
},
],
default: 'open',
description: 'Filter orders by their status',
},
{
displayName: 'Since ID',
name: 'sinceId',
type: 'string',
default: '',
description: 'Show orders after the specified ID',
},
{
displayName: 'Updated At Max',
name: 'updatedAtMax',
type: 'dateTime',
default: '',
description: 'Show orders last updated at or after date',
},
{
displayName: 'Updated At Min',
name: 'updatedAtMin',
type: 'dateTime',
default: '',
description: 'Show orders last updated at or before date',
},
],
},
/* -------------------------------------------------------------------------- */
/* order:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Order ID',
name: 'orderId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['order'],
operation: ['update'],
},
},
required: true,
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['order'],
},
},
default: {},
options: [
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description: "The customer's email address",
},
{
displayName: 'Location Name or ID',
name: 'locationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLocations',
},
default: '',
description:
'The ID of the physical location where the order was processed. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
description: 'An optional note that a shop owner can attach to the order',
},
{
displayName: 'Shipping Address',
name: 'shippingAddressUi',
placeholder: 'Add Shipping',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
name: 'shippingAddressValues',
displayName: 'Shipping Address',
values: [
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
},
{
displayName: 'Company',
name: 'company',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
{
displayName: 'Address Line 1',
name: 'address1',
type: 'string',
default: '',
},
{
displayName: 'Address Line 2',
name: 'address2',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'Province',
name: 'province',
type: 'string',
default: '',
},
{
displayName: 'Zip Code',
name: 'zip',
type: 'string',
default: '',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Source Name',
name: 'sourceName',
type: 'string',
default: '',
description:
'Where the order originated. Can be set only during order creation, and is not writeable afterwards.',
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
description: 'Tags attached to the order, formatted as a string of comma-separated values',
},
],
},
];
@@ -0,0 +1,45 @@
export interface ILineItem {
id?: number;
product_id?: number;
variant_id?: number;
title?: string;
price?: string;
grams?: string;
quantity?: number;
}
export interface IDiscountCode {
code?: string;
amount?: string;
type?: string;
}
export interface IAddress {
first_name?: string;
last_name?: string;
company?: string;
address1?: string;
address2?: string;
city?: string;
province?: string;
country?: string;
phone?: string;
zip?: string;
}
export interface IOrder {
billing_address?: IAddress;
discount_codes?: IDiscountCode[];
email?: string;
fulfillment_status?: string;
inventory_behaviour?: string;
line_items?: ILineItem[];
location_id?: number;
note?: string;
send_fulfillment_receipt?: boolean;
send_receipt?: boolean;
shipping_address?: IAddress;
source_name?: string;
tags?: string;
test?: boolean;
}
@@ -0,0 +1,764 @@
import type { INodeProperties } from 'n8n-workflow';
export const productOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['product'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a product',
action: 'Create a product',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a product',
action: 'Delete a product',
},
{
name: 'Get',
value: 'get',
description: 'Get a product',
action: 'Get a product',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many products',
action: 'Get many products',
},
{
name: 'Update',
value: 'update',
description: 'Update a product',
action: 'Update a product',
},
],
default: 'create',
},
];
export const productFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* product:create/update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Title',
name: 'title',
type: 'string',
placeholder: '',
displayOptions: {
show: {
operation: ['create'],
resource: ['product'],
},
},
default: '',
description: 'The name of the product',
required: true,
},
{
displayName: 'Product ID',
name: 'productId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['product'],
operation: ['update'],
},
},
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['product'],
},
},
default: {},
options: [
{
displayName: 'Body HTML',
name: 'body_html',
type: 'string',
default: '',
description: 'A description of the product. Supports HTML formatting.',
},
{
displayName: 'Handle',
name: 'handle',
type: 'string',
default: '',
description:
"A unique human-friendly string for the product. Automatically generated from the product's title. Used by the Liquid templating language to refer to objects.",
},
{
displayName: 'Images',
name: 'images',
type: 'collection',
placeholder: 'Add Image Field',
typeOptions: {
multipleValues: true,
},
default: {},
description:
'A list of product image objects, each one representing an image associated with the product',
options: [
{
displayName: 'Created At',
name: 'created_at',
type: 'dateTime',
default: '',
description: 'The date and time when the product image was created',
},
{
displayName: 'ID',
name: 'id',
type: 'number',
default: '',
description: 'A unique numeric identifier for the product image',
},
{
displayName: 'Position',
name: 'position',
type: 'number',
default: '',
description:
'The order of the product image in the list. The first product image is at position 1 and is the "main" image for the product.',
},
{
displayName: 'Product ID',
name: 'product_id',
type: 'number',
default: '',
description: 'The ID of the product associated with the image',
},
{
displayName: 'Variant IDs',
name: 'variant_ids',
type: 'number',
typeOptions: {
multipleValues: true,
},
default: '',
description: 'An array of variant IDs associated with the image',
},
{
displayName: 'Source',
name: 'src',
type: 'string',
default: '',
description:
'<p>Specifies the location of the product image. This parameter supports URL filters that you can use to retrieve modified copies of the image.</p><p>For example, add _small, to the filename to retrieve a scaled copy of the image at 100 x 100 px (for example, ipod-nano_small.png), or add _2048x2048 to retrieve a copy of the image constrained at 2048 x 2048 px resolution (for example, ipod-nano_2048x2048.png).</p>.',
},
{
displayName: 'Width',
name: 'width',
type: 'number',
default: '',
description: 'Width dimension of the image which is determined on upload',
},
{
displayName: 'Height',
name: 'height',
type: 'number',
default: '',
description: 'Height dimension of the image which is determined on upload',
},
{
displayName: 'Updated At',
name: 'updated_at',
type: 'dateTime',
default: '',
description: 'The date and time when the product image was last modified',
},
],
},
{
displayName: 'Options',
name: 'productOptions',
type: 'fixedCollection',
placeholder: 'Add option',
typeOptions: {
multipleValues: true,
},
default: {},
description:
'The custom product property names like Size, Color, and Material. You can add up to 3 options of up to 255 characters each.',
options: [
{
displayName: 'Option',
name: 'option',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: "Option's name",
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: "Option's values",
},
],
},
],
},
{
displayName: 'Product Type',
name: 'product_type',
type: 'string',
default: '',
description: 'A categorization for the product used for filtering and searching products',
},
{
displayName: 'Published At',
name: 'published_at',
type: 'dateTime',
default: '',
description:
'The date and time (ISO 8601 format) when the product was published. Can be set to null to unpublish the product from the Online Store channel.',
},
{
displayName: 'Published Scope',
name: 'published_scope',
type: 'options',
default: '',
options: [
{
name: 'Global',
value: 'global',
description:
'The product is published to both the Online Store channel and the Point of Sale channel',
},
{
name: 'Web',
value: 'web',
description:
'The product is published to the Online Store channel but not published to the Point of Sale channel',
},
],
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
description:
'A string of comma-separated tags that are used for filtering and search. A product can have up to 250 tags. Each tag can have up to 255 characters.',
},
{
displayName: 'Template Suffix',
name: 'template_suffix',
type: 'string',
default: '',
description:
'The suffix of the Liquid template used for the product page. If this property is specified, then the product page uses a template called "product.suffix.liquid", where "suffix" is the value of this property. If this property is "" or null, then the product page uses the default template "product.liquid". (default: null)',
},
// {
// displayName: 'Variants',
// name: 'variants',
// type: 'collection',
// placeholder: 'Add Variant Field',
// typeOptions: {
// multipleValues: true,
// },
// default: {},
// description: 'A list of product variants, each representing a different version of the product.',
// options: [
// {
// displayName: 'Created At',
// name: 'created_at',
// type: 'dateTime',
// default: '',
// description: 'The date and time when the product image was created.',
// },
// ],
// },
{
displayName: 'Vendor',
name: 'vendor',
type: 'string',
default: '',
description: "The name of the product's vendor",
},
],
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['update'],
resource: ['product'],
},
},
default: {},
options: [
{
displayName: 'Body HTML',
name: 'body_html',
type: 'string',
default: '',
description: 'A description of the product. Supports HTML formatting.',
},
{
displayName: 'Handle',
name: 'handle',
type: 'string',
default: '',
description:
"A unique human-friendly string for the product. Automatically generated from the product's title. Used by the Liquid templating language to refer to objects.",
},
{
displayName: 'Images',
name: 'images',
type: 'collection',
placeholder: 'Add Image Field',
typeOptions: {
multipleValues: true,
},
default: {},
description:
'A list of product image objects, each one representing an image associated with the product',
options: [
{
displayName: 'Created At',
name: 'created_at',
type: 'dateTime',
default: '',
description: 'The date and time when the product image was created',
},
{
displayName: 'ID',
name: 'id',
type: 'number',
default: '',
description: 'A unique numeric identifier for the product image',
},
{
displayName: 'Position',
name: 'position',
type: 'number',
default: '',
description:
'The order of the product image in the list. The first product image is at position 1 and is the "main" image for the product.',
},
{
displayName: 'Product ID',
name: 'product_id',
type: 'number',
default: '',
description: 'The ID of the product associated with the image',
},
{
displayName: 'Variant IDs',
name: 'variant_ids',
type: 'number',
typeOptions: {
multipleValues: true,
},
default: '',
description: 'An array of variant IDs associated with the image',
},
{
displayName: 'Source',
name: 'src',
type: 'string',
default: '',
description:
'<p>Specifies the location of the product image. This parameter supports URL filters that you can use to retrieve modified copies of the image.</p><p>For example, add _small, to the filename to retrieve a scaled copy of the image at 100 x 100 px (for example, ipod-nano_small.png), or add _2048x2048 to retrieve a copy of the image constrained at 2048 x 2048 px resolution (for example, ipod-nano_2048x2048.png).</p>.',
},
{
displayName: 'Width',
name: 'width',
type: 'number',
default: '',
description: 'Width dimension of the image which is determined on upload',
},
{
displayName: 'Height',
name: 'height',
type: 'number',
default: '',
description: 'Height dimension of the image which is determined on upload',
},
{
displayName: 'Updated At',
name: 'updated_at',
type: 'dateTime',
default: '',
description: 'The date and time when the product image was last modified',
},
],
},
{
displayName: 'Options',
name: 'productOptions',
type: 'fixedCollection',
placeholder: 'Add option',
typeOptions: {
multipleValues: true,
},
default: {},
description:
'The custom product property names like Size, Color, and Material. You can add up to 3 options of up to 255 characters each.',
options: [
{
displayName: 'Option',
name: 'option',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: "Option's name",
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: "Option's values",
},
],
},
],
},
{
displayName: 'Product Type',
name: 'product_type',
type: 'string',
default: '',
description: 'A categorization for the product used for filtering and searching products',
},
{
displayName: 'Published At',
name: 'published_at',
type: 'dateTime',
default: '',
description:
'The date and time (ISO 8601 format) when the product was published. Can be set to null to unpublish the product from the Online Store channel.',
},
{
displayName: 'Published Scope',
name: 'published_scope',
type: 'options',
default: '',
options: [
{
name: 'Global',
value: 'global',
description:
'The product is published to both the Online Store channel and the Point of Sale channel',
},
{
name: 'Web',
value: 'web',
description:
'The product is published to the Online Store channel but not published to the Point of Sale channel',
},
],
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
description:
'A string of comma-separated tags that are used for filtering and search. A product can have up to 250 tags. Each tag can have up to 255 characters.',
},
{
displayName: 'Template Suffix',
name: 'template_suffix',
type: 'string',
default: '',
description:
'The suffix of the Liquid template used for the product page. If this property is specified, then the product page uses a template called "product.suffix.liquid", where "suffix" is the value of this property. If this property is "" or null, then the product page uses the default template "product.liquid". (default: null)',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'The name of the product',
},
// {
// displayName: 'Variants',
// name: 'variants',
// type: 'collection',
// placeholder: 'Add Variant Field',
// typeOptions: {
// multipleValues: true,
// },
// default: {},
// description: 'A list of product variants, each representing a different version of the product.',
// options: [
// {
// displayName: 'Created At',
// name: 'created_at',
// type: 'dateTime',
// default: '',
// description: 'The date and time when the product image was created.',
// },
// ],
// },
{
displayName: 'Vendor',
name: 'vendor',
type: 'string',
default: '',
description: "The name of the product's vendor",
},
],
},
/* -------------------------------------------------------------------------- */
/* product:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Product ID',
name: 'productId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['product'],
operation: ['delete'],
},
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* product:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Product ID',
name: 'productId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['product'],
operation: ['get'],
},
},
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['get'],
resource: ['product'],
},
},
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description:
'Fields the product will return, formatted as a string of comma-separated values. By default all the fields are returned.',
},
],
},
/* -------------------------------------------------------------------------- */
/* product:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['product'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['product'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 250,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
resource: ['product'],
},
},
options: [
{
displayName: 'Collection ID',
name: 'collection_id',
type: 'string',
default: '',
description: 'Filter results by product collection ID',
},
{
displayName: 'Created At Max',
name: 'created_at_max',
type: 'dateTime',
default: '',
description: 'Show products created before date',
},
{
displayName: 'Created At Min',
name: 'created_at_min',
type: 'dateTime',
default: '',
description: 'Show products created after date',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Show only certain fields, specified by a comma-separated list of field names',
},
{
displayName: 'Handle',
name: 'handle',
type: 'string',
default: '',
description: 'Filter results by product handle',
},
{
displayName: 'IDs',
name: 'ids',
type: 'string',
default: '',
description: 'Return only products specified by a comma-separated list of product IDs',
},
{
displayName: 'Presentment Currencies',
name: 'presentment_currencies',
type: 'string',
default: '',
description:
'Return presentment prices in only certain currencies, specified by a comma-separated list of ISO 4217 currency codes',
},
{
displayName: 'Product Type',
name: 'product_type',
type: 'string',
default: '',
description: 'Filter results by product type',
},
{
displayName: 'Published At Max',
name: 'published_at_max',
type: 'dateTime',
default: '',
description: 'Show products published before date',
},
{
displayName: 'Published At Min',
name: 'published_at_min',
type: 'dateTime',
default: '',
description: 'Show products published after date',
},
{
displayName: 'Published Status',
name: 'published_status',
type: 'options',
options: [
{
name: 'Any',
value: 'any',
description: 'Show all products',
},
{
name: 'Published',
value: 'published',
description: 'Show only published products',
},
{
name: 'Unpublished',
value: 'unpublished',
description: 'Show only unpublished products',
},
],
default: 'any',
description: 'Return products by their published status',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Filter results by product title',
},
{
displayName: 'Updated At Max',
name: 'updated_at_max',
type: 'dateTime',
default: '',
description: 'Show products last updated before date',
},
{
displayName: 'Updated At Min',
name: 'updated_at_min',
type: 'dateTime',
default: '',
description: 'Show products last updated after date',
},
{
displayName: 'Vendor',
name: 'vendor',
type: 'string',
default: '',
description: 'Filter results by product vendor',
},
],
},
];
@@ -0,0 +1,64 @@
import type { IDataObject } from 'n8n-workflow';
export interface IImage {
id?: string;
product_id?: string;
position?: number;
created_at?: string;
updated_at?: string;
width?: number;
height?: number;
src?: string;
variant_ids?: number[];
}
export interface IPrice {
currency_code?: string;
amount?: string;
}
export interface IPresentmentPrices {
price?: IPrice;
compare_at_price?: IPrice;
}
export interface IVariant {
barcode?: string;
compare_at_price?: string;
created_at?: string;
fulfillment_service?: string;
grams?: number;
id?: number;
image_id?: number;
inventory_item_id?: number;
inventory_management?: string;
inventory_policy?: string;
option1?: string;
option2?: string;
option3?: string;
presentment_prices?: IPresentmentPrices[];
price?: string;
product_id?: number;
sku?: string;
taxable?: boolean;
tax_code?: string;
title?: string;
updated_at?: string;
weight?: number;
weight_unit?: string;
}
export interface IProduct {
body_html?: string;
handle?: string;
images?: IImage[];
options?: IDataObject[];
product_type?: string;
published_at?: string;
published_scope?: string;
tags?: string;
template_suffix?: string;
title?: string;
variants?: IVariant[];
vendor?: string;
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.shopify",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Sales"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/shopify/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.shopify/"
}
]
}
}
@@ -0,0 +1,479 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { keysToSnakeCase, shopifyApiRequest, shopifyApiRequestAllItems } from './GenericFunctions';
import { orderFields, orderOperations } from './OrderDescription';
import type { IAddress, IDiscountCode, ILineItem, IOrder } from './OrderInterface';
import { productFields, productOperations } from './ProductDescription';
import type { IProduct } from './ProductInterface';
export class Shopify implements INodeType {
description: INodeTypeDescription = {
displayName: 'Shopify',
name: 'shopify',
icon: 'file:shopify.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Shopify API',
defaults: {
name: 'Shopify',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'shopifyApi',
required: true,
displayOptions: {
show: {
authentication: ['apiKey'],
},
},
},
{
name: 'shopifyAccessTokenApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'shopifyOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
properties: [
{
displayName: 'Shopify API Version: 2024-07',
type: 'notice',
name: 'apiVersion',
default: '',
isNodeSetting: true,
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'API Key',
value: 'apiKey',
},
],
default: 'apiKey',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Order',
value: 'order',
},
{
name: 'Product',
value: 'product',
},
],
default: 'order',
},
// ORDER
...orderOperations,
...orderFields,
// PRODUCTS
...productOperations,
...productFields,
],
};
methods = {
loadOptions: {
// Get all the available products to display them to user so that they can
// select them easily
async getProducts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const products = await shopifyApiRequestAllItems.call(
this,
'products',
'GET',
'/products.json',
{},
{ fields: 'id,title' },
);
for (const product of products) {
const productName = product.title;
const productId = product.id;
returnData.push({
name: productName,
value: productId,
});
}
return returnData;
},
// Get all the available locations to display them to user so that they can
// select them easily
async getLocations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const locations = await shopifyApiRequestAllItems.call(
this,
'locations',
'GET',
'/locations.json',
{},
{ fields: 'id,name' },
);
for (const location of locations) {
const locationName = location.name;
const locationId = location.id;
returnData.push({
name: locationName,
value: locationId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
let responseData;
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
try {
if (resource === 'order') {
//https://shopify.dev/docs/admin-api/rest/reference/orders/order#create-2020-04
if (operation === 'create') {
const additionalFields = this.getNodeParameter('additionalFields', i);
const discount = additionalFields.discountCodesUi as IDataObject;
const billing = additionalFields.billingAddressUi as IDataObject;
const shipping = additionalFields.shippingAddressUi as IDataObject;
const lineItem = (this.getNodeParameter('limeItemsUi', i) as IDataObject)
.lineItemValues as IDataObject[];
if (lineItem === undefined) {
throw new NodeOperationError(
this.getNode(),
'At least one line item has to be added',
{ itemIndex: i },
);
}
const body: IOrder = {
test: true,
line_items: keysToSnakeCase(lineItem) as ILineItem[],
};
if (additionalFields.fulfillmentStatus) {
body.fulfillment_status = additionalFields.fulfillmentStatus as string;
}
if (additionalFields.inventoryBehaviour) {
body.inventory_behaviour = additionalFields.inventoryBehaviour as string;
}
if (additionalFields.locationId) {
body.location_id = additionalFields.locationId as number;
}
if (additionalFields.note) {
body.note = additionalFields.note as string;
}
if (additionalFields.sendFulfillmentReceipt) {
body.send_fulfillment_receipt = additionalFields.sendFulfillmentReceipt as boolean;
}
if (additionalFields.sendReceipt) {
body.send_receipt = additionalFields.sendReceipt as boolean;
}
if (additionalFields.sendReceipt) {
body.send_receipt = additionalFields.sendReceipt as boolean;
}
if (additionalFields.sourceName) {
body.source_name = additionalFields.sourceName as string;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string;
}
if (additionalFields.test) {
body.test = additionalFields.test as boolean;
}
if (additionalFields.email) {
body.email = additionalFields.email as string;
}
if (discount) {
body.discount_codes = discount.discountCodesValues as IDiscountCode[];
}
if (billing) {
body.billing_address = keysToSnakeCase(
billing.billingAddressValues as IDataObject,
)[0] as IAddress;
}
if (shipping) {
body.shipping_address = keysToSnakeCase(
shipping.shippingAddressValues as IDataObject,
)[0] as IAddress;
}
responseData = await shopifyApiRequest.call(this, 'POST', '/orders.json', {
order: body,
});
responseData = responseData.order;
}
//https://shopify.dev/docs/admin-api/rest/reference/orders/order#destroy-2020-04
if (operation === 'delete') {
const orderId = this.getNodeParameter('orderId', i) as string;
responseData = await shopifyApiRequest.call(this, 'DELETE', `/orders/${orderId}.json`);
responseData = { success: true };
}
//https://shopify.dev/docs/admin-api/rest/reference/orders/order#show-2020-04
if (operation === 'get') {
const orderId = this.getNodeParameter('orderId', i) as string;
const options = this.getNodeParameter('options', i);
if (options.fields) {
qs.fields = options.fields as string;
}
responseData = await shopifyApiRequest.call(
this,
'GET',
`/orders/${orderId}.json`,
{},
qs,
);
responseData = responseData.order;
}
//https://shopify.dev/docs/admin-api/rest/reference/orders/order#index-2020-04
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const options = this.getNodeParameter('options', i);
if (options.fields) {
qs.fields = options.fields as string;
}
if (options.attributionAppId) {
qs.attribution_app_id = options.attributionAppId as string;
}
if (options.createdAtMin) {
qs.created_at_min = options.createdAtMin as string;
}
if (options.createdAtMax) {
qs.created_at_max = options.createdAtMax as string;
}
if (options.updatedAtMax) {
qs.updated_at_max = options.updatedAtMax as string;
}
if (options.updatedAtMin) {
qs.updated_at_min = options.updatedAtMin as string;
}
if (options.processedAtMin) {
qs.processed_at_min = options.processedAtMin as string;
}
if (options.processedAtMax) {
qs.processed_at_max = options.processedAtMax as string;
}
if (options.sinceId) {
qs.since_id = options.sinceId as string;
}
if (options.ids) {
qs.ids = options.ids as string;
}
if (options.status) {
qs.status = options.status as string;
}
if (options.financialStatus) {
qs.financial_status = options.financialStatus as string;
}
if (options.fulfillmentStatus) {
qs.fulfillment_status = options.fulfillmentStatus as string;
}
if (returnAll) {
responseData = await shopifyApiRequestAllItems.call(
this,
'orders',
'GET',
'/orders.json',
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i);
responseData = await shopifyApiRequest.call(this, 'GET', '/orders.json', {}, qs);
responseData = responseData.orders;
}
}
//https://shopify.dev/docs/admin-api/rest/reference/orders/order#update-2019-10
if (operation === 'update') {
const orderId = this.getNodeParameter('orderId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const shipping = updateFields.shippingAddressUi as IDataObject;
const body: IOrder = {};
if (updateFields.locationId) {
body.location_id = updateFields.locationId as number;
}
if (updateFields.note) {
body.note = updateFields.note as string;
}
if (updateFields.sourceName) {
body.source_name = updateFields.sourceName as string;
}
if (updateFields.tags) {
body.tags = updateFields.tags as string;
}
if (updateFields.email) {
body.email = updateFields.email as string;
}
if (shipping) {
body.shipping_address = keysToSnakeCase(
shipping.shippingAddressValues as IDataObject,
)[0] as IAddress;
}
responseData = await shopifyApiRequest.call(this, 'PUT', `/orders/${orderId}.json`, {
order: body,
});
responseData = responseData.order;
}
} else if (resource === 'product') {
const productId = this.getNodeParameter('productId', i, '') as string;
let body: IProduct = {};
//https://shopify.dev/docs/admin-api/rest/reference/products/product#create-2020-04
if (operation === 'create') {
const title = this.getNodeParameter('title', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i, {});
if (additionalFields.productOptions) {
const metadata = (additionalFields.productOptions as IDataObject)
.option as IDataObject[];
additionalFields.options = {};
for (const data of metadata) {
//@ts-ignore
additionalFields.options[data.name as string] = data.value;
}
delete additionalFields.productOptions;
}
body = additionalFields;
body.title = title;
responseData = await shopifyApiRequest.call(this, 'POST', '/products.json', {
product: body,
});
responseData = responseData.product;
}
if (operation === 'delete') {
//https://shopify.dev/docs/admin-api/rest/reference/products/product#destroy-2020-04
responseData = await shopifyApiRequest.call(
this,
'DELETE',
`/products/${productId}.json`,
);
responseData = { success: true };
}
if (operation === 'get') {
//https://shopify.dev/docs/admin-api/rest/reference/products/product#show-2020-04
const additionalFields = this.getNodeParameter('additionalFields', i, {});
Object.assign(qs, additionalFields);
responseData = await shopifyApiRequest.call(
this,
'GET',
`/products/${productId}.json`,
{},
qs,
);
responseData = responseData.product;
}
if (operation === 'getAll') {
//https://shopify.dev/docs/admin-api/rest/reference/products/product#index-2020-04
const additionalFields = this.getNodeParameter('additionalFields', i, {});
const returnAll = this.getNodeParameter('returnAll', i);
Object.assign(qs, additionalFields);
if (returnAll) {
responseData = await shopifyApiRequestAllItems.call(
this,
'products',
'GET',
'/products.json',
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i);
responseData = await shopifyApiRequest.call(this, 'GET', '/products.json', {}, qs);
responseData = responseData.products;
}
}
if (operation === 'update') {
//https://shopify.dev/docs/admin-api/rest/reference/products/product?api[version]=2020-07#update-2020-07
const updateFields = this.getNodeParameter('updateFields', i, {});
if (updateFields.productOptions) {
const metadata = (updateFields.productOptions as IDataObject).option as IDataObject[];
updateFields.options = {};
for (const data of metadata) {
//@ts-ignore
updateFields.options[data.name as string] = data.value;
}
delete updateFields.productOptions;
}
body = updateFields;
responseData = await shopifyApiRequest.call(
this,
'PUT',
`/products/${productId}.json`,
{ product: body },
);
responseData = responseData.product;
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,25 @@
{
"node": "n8n-nodes-base.shopifyTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Sales"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/shopify/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.shopifytrigger/"
}
],
"generic": [
{
"label": "6 e-commerce workflows to power up your Shopify s",
"icon": "store",
"url": "https://n8n.io/blog/no-code-ecommerce-workflow-automations/"
}
]
}
}
@@ -0,0 +1,432 @@
import { createHmac } from 'crypto';
import {
type IHookFunctions,
type IWebhookFunctions,
type IDataObject,
type INodeType,
type INodeTypeDescription,
type IWebhookResponseData,
NodeConnectionTypes,
} from 'n8n-workflow';
import { shopifyApiRequest } from './GenericFunctions';
export class ShopifyTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Shopify Trigger',
name: 'shopifyTrigger',
icon: 'file:shopify.svg',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["event"]}}',
description: 'Handle Shopify events via webhooks',
defaults: {
name: 'Shopify Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'shopifyApi',
required: true,
displayOptions: {
show: {
authentication: ['apiKey'],
},
},
},
{
name: 'shopifyAccessTokenApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'shopifyOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'API Key',
value: 'apiKey',
},
],
default: 'apiKey',
},
{
displayName: 'Trigger On',
name: 'topic',
type: 'options',
default: '',
options: [
{
name: 'App Uninstalled',
value: 'app/uninstalled',
},
{
name: 'Cart Created',
value: 'carts/create',
},
{
name: 'Cart Updated',
value: 'carts/update',
},
{
name: 'Checkout Created',
value: 'checkouts/create',
},
{
name: 'Checkout Delete',
value: 'checkouts/delete',
},
{
name: 'Checkout Update',
value: 'checkouts/update',
},
{
name: 'Collection Created',
value: 'collections/create',
},
{
name: 'Collection Deleted',
value: 'collections/delete',
},
{
name: 'Collection Listings Added',
value: 'collection_listings/add',
},
{
name: 'Collection Listings Removed',
value: 'collection_listings/remove',
},
{
name: 'Collection Listings Updated',
value: 'collection_listings/update',
},
{
name: 'Collection Updated',
value: 'collections/update',
},
{
name: 'Customer Created',
value: 'customers/create',
},
{
name: 'Customer Deleted',
value: 'customers/delete',
},
{
name: 'Customer Disabled',
value: 'customers/disable',
},
{
name: 'Customer Enabled',
value: 'customers/enable',
},
{
name: 'Customer Groups Created',
value: 'customer_groups/create',
},
{
name: 'Customer Groups Deleted',
value: 'customer_groups/delete',
},
{
name: 'Customer Groups Updated',
value: 'customer_groups/update',
},
{
name: 'Customer Updated',
value: 'customers/update',
},
{
name: 'Draft Orders Created',
value: 'draft_orders/create',
},
{
name: 'Draft Orders Deleted',
value: 'draft_orders/delete',
},
{
name: 'Draft Orders Updated',
value: 'draft_orders/update',
},
{
name: 'Fulfillment Created',
value: 'fulfillments/create',
},
{
name: 'Fulfillment Events Created',
value: 'fulfillment_events/create',
},
{
name: 'Fulfillment Events Deleted',
value: 'fulfillment_events/delete',
},
{
name: 'Fulfillment Updated',
value: 'fulfillments/update',
},
{
name: 'Inventory Items Created',
value: 'inventory_items/create',
},
{
name: 'Inventory Items Deleted',
value: 'inventory_items/delete',
},
{
name: 'Inventory Items Updated',
value: 'inventory_items/update',
},
{
name: 'Inventory Levels Connected',
value: 'inventory_levels/connect',
},
{
name: 'Inventory Levels Disconnected',
value: 'inventory_levels/disconnect',
},
{
name: 'Inventory Levels Updated',
value: 'inventory_levels/update',
},
{
name: 'Locale Created',
value: 'locales/create',
},
{
name: 'Locale Updated',
value: 'locales/update',
},
{
name: 'Location Created',
value: 'locations/create',
},
{
name: 'Location Deleted',
value: 'locations/delete',
},
{
name: 'Location Updated',
value: 'locations/update',
},
{
name: 'Order Cancelled',
value: 'orders/cancelled',
},
{
name: 'Order Created',
value: 'orders/create',
},
{
name: 'Order Fulfilled',
value: 'orders/fulfilled',
},
{
name: 'Order Paid',
value: 'orders/paid',
},
{
name: 'Order Partially Fulfilled',
value: 'orders/partially_fulfilled',
},
{
name: 'Order Transactions Created',
value: 'order_transactions/create',
},
{
name: 'Order Updated',
value: 'orders/updated',
},
{
name: 'Orders Deleted',
value: 'orders/delete',
},
{
name: 'Product Created',
value: 'products/create',
},
{
name: 'Product Deleted',
value: 'products/delete',
},
{
name: 'Product Listings Added',
value: 'product_listings/add',
},
{
name: 'Product Listings Removed',
value: 'product_listings/remove',
},
{
name: 'Product Listings Updated',
value: 'product_listings/update',
},
{
name: 'Product Updated',
value: 'products/update',
},
{
name: 'Refund Created',
value: 'refunds/create',
},
{
name: 'Shop Updated',
value: 'shop/update',
},
{
name: 'Tender Transactions Created',
value: 'tender_transactions/create',
},
{
name: 'Theme Created',
value: 'themes/create',
},
{
name: 'Theme Deleted',
value: 'themes/delete',
},
{
name: 'Theme Published',
value: 'themes/publish',
},
{
name: 'Theme Updated',
value: 'themes/update',
},
],
},
],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const topic = this.getNodeParameter('topic') as string;
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const endpoint = '/webhooks';
const { webhooks } = await shopifyApiRequest.call(this, 'GET', endpoint, {}, { topic });
for (const webhook of webhooks) {
if (webhook.address === webhookUrl) {
webhookData.webhookId = webhook.id;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const topic = this.getNodeParameter('topic') as string;
const webhookData = this.getWorkflowStaticData('node');
const endpoint = '/webhooks.json';
const body = {
webhook: {
topic,
address: webhookUrl,
format: 'json',
},
};
const responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body);
if (responseData.webhook === undefined || responseData.webhook.id === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.webhook.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/webhooks/${webhookData.webhookId}.json`;
try {
await shopifyApiRequest.call(this, 'DELETE', endpoint, {});
} catch (error) {
return false;
}
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const headerData = this.getHeaderData() as IDataObject;
const req = this.getRequestObject();
const authentication = this.getNodeParameter('authentication') as string;
let secret = '';
if (authentication === 'apiKey') {
const credentials = await this.getCredentials('shopifyApi');
secret = credentials.sharedSecret as string;
}
if (authentication === 'accessToken') {
const credentials = await this.getCredentials('shopifyAccessTokenApi');
secret = credentials.appSecretKey as string;
}
if (authentication === 'oAuth2') {
const credentials = await this.getCredentials('shopifyOAuth2Api');
secret = credentials.clientSecret as string;
}
const topic = this.getNodeParameter('topic') as string;
if (
headerData['x-shopify-topic'] !== undefined &&
headerData['x-shopify-hmac-sha256'] !== undefined &&
headerData['x-shopify-shop-domain'] !== undefined &&
headerData['x-shopify-api-version'] !== undefined
) {
const computedSignature = createHmac('sha256', secret).update(req.rawBody).digest('base64');
if (headerData['x-shopify-hmac-sha256'] !== computedSignature) {
return {};
}
if (topic !== headerData['x-shopify-topic']) {
return {};
}
} else {
return {};
}
return {
workflowData: [this.helpers.returnJsonArray(req.body as IDataObject)],
};
}
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"id": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,218 @@
{
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"current_quantity": {
"type": "integer"
},
"discount_allocations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"amount_set": {
"type": "object",
"properties": {
"presentment_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
},
"shop_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
}
}
},
"discount_application_index": {
"type": "integer"
}
}
}
},
"fulfillable_quantity": {
"type": "integer"
},
"fulfillment_service": {
"type": "string"
},
"gift_card": {
"type": "boolean"
},
"grams": {
"type": "integer"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"price": {
"type": "string"
},
"price_set": {
"type": "object",
"properties": {
"presentment_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
},
"shop_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
}
}
},
"product_exists": {
"type": "boolean"
},
"properties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
},
"quantity": {
"type": "integer"
},
"requires_shipping": {
"type": "boolean"
},
"tax_lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"channel_liable": {
"type": "boolean"
},
"price": {
"type": "string"
},
"price_set": {
"type": "object",
"properties": {
"presentment_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
},
"shop_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
}
}
},
"title": {
"type": "string"
}
}
}
},
"taxable": {
"type": "boolean"
},
"title": {
"type": "string"
},
"total_discount": {
"type": "string"
},
"total_discount_set": {
"type": "object",
"properties": {
"presentment_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
},
"shop_money": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency_code": {
"type": "string"
}
}
}
}
},
"vendor": {
"type": "string"
}
}
}
}
},
"version": 6
}
@@ -0,0 +1,189 @@
{
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"handle": {
"type": "string"
},
"id": {
"type": "integer"
},
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"alt": {
"type": "null"
},
"created_at": {
"type": "string"
},
"height": {
"type": "integer"
},
"id": {
"type": "integer"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"src": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"width": {
"type": "integer"
}
}
}
},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"product_type": {
"type": "string"
},
"published_scope": {
"type": "string"
},
"status": {
"type": "string"
},
"tags": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"barcode": {
"type": "null"
},
"compare_at_price": {
"type": "null"
},
"created_at": {
"type": "string"
},
"fulfillment_service": {
"type": "string"
},
"grams": {
"type": "integer"
},
"id": {
"type": "integer"
},
"image_id": {
"type": "null"
},
"inventory_item_id": {
"type": "integer"
},
"inventory_management": {
"type": "null"
},
"inventory_policy": {
"type": "string"
},
"inventory_quantity": {
"type": "integer"
},
"old_inventory_quantity": {
"type": "integer"
},
"option1": {
"type": "string"
},
"option2": {
"type": "null"
},
"option3": {
"type": "null"
},
"position": {
"type": "integer"
},
"price": {
"type": "string"
},
"product_id": {
"type": "integer"
},
"requires_shipping": {
"type": "boolean"
},
"sku": {
"type": "string"
},
"taxable": {
"type": "boolean"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"weight": {
"type": "integer"
},
"weight_unit": {
"type": "string"
}
}
}
},
"vendor": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,206 @@
{
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"handle": {
"type": "string"
},
"id": {
"type": "integer"
},
"image": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"height": {
"type": "integer"
},
"id": {
"type": "integer"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"src": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variant_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"width": {
"type": "integer"
}
}
},
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"height": {
"type": "integer"
},
"id": {
"type": "integer"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"src": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variant_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"width": {
"type": "integer"
}
}
}
},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"product_type": {
"type": "string"
},
"published_scope": {
"type": "string"
},
"status": {
"type": "string"
},
"tags": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"fulfillment_service": {
"type": "string"
},
"grams": {
"type": "integer"
},
"id": {
"type": "integer"
},
"inventory_item_id": {
"type": "integer"
},
"inventory_policy": {
"type": "string"
},
"inventory_quantity": {
"type": "integer"
},
"old_inventory_quantity": {
"type": "integer"
},
"option1": {
"type": "string"
},
"position": {
"type": "integer"
},
"price": {
"type": "string"
},
"product_id": {
"type": "integer"
},
"requires_shipping": {
"type": "boolean"
},
"taxable": {
"type": "boolean"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"weight_unit": {
"type": "string"
}
}
}
},
"vendor": {
"type": "string"
}
},
"version": 4
}
@@ -0,0 +1,168 @@
{
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"handle": {
"type": "string"
},
"id": {
"type": "integer"
},
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"height": {
"type": "integer"
},
"id": {
"type": "integer"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"src": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variant_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"width": {
"type": "integer"
}
}
}
},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"product_type": {
"type": "string"
},
"published_scope": {
"type": "string"
},
"status": {
"type": "string"
},
"tags": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"fulfillment_service": {
"type": "string"
},
"grams": {
"type": "integer"
},
"id": {
"type": "integer"
},
"inventory_item_id": {
"type": "integer"
},
"inventory_policy": {
"type": "string"
},
"inventory_quantity": {
"type": "integer"
},
"old_inventory_quantity": {
"type": "integer"
},
"option1": {
"type": "string"
},
"position": {
"type": "integer"
},
"price": {
"type": "string"
},
"product_id": {
"type": "integer"
},
"requires_shipping": {
"type": "boolean"
},
"taxable": {
"type": "boolean"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"weight_unit": {
"type": "string"
}
}
}
},
"vendor": {
"type": "string"
}
},
"version": 2
}
@@ -0,0 +1,212 @@
{
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"body_html": {
"type": "string"
},
"created_at": {
"type": "string"
},
"handle": {
"type": "string"
},
"id": {
"type": "integer"
},
"image": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"height": {
"type": "integer"
},
"id": {
"type": "integer"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"src": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variant_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"width": {
"type": "integer"
}
}
},
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"height": {
"type": "integer"
},
"id": {
"type": "integer"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"src": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variant_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"width": {
"type": "integer"
}
}
}
},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"position": {
"type": "integer"
},
"product_id": {
"type": "integer"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"product_type": {
"type": "string"
},
"published_scope": {
"type": "string"
},
"status": {
"type": "string"
},
"tags": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"admin_graphql_api_id": {
"type": "string"
},
"created_at": {
"type": "string"
},
"fulfillment_service": {
"type": "string"
},
"grams": {
"type": "integer"
},
"id": {
"type": "integer"
},
"inventory_item_id": {
"type": "integer"
},
"inventory_policy": {
"type": "string"
},
"inventory_quantity": {
"type": "integer"
},
"old_inventory_quantity": {
"type": "integer"
},
"option1": {
"type": "string"
},
"option3": {
"type": "null"
},
"position": {
"type": "integer"
},
"price": {
"type": "string"
},
"product_id": {
"type": "integer"
},
"requires_shipping": {
"type": "boolean"
},
"taxable": {
"type": "boolean"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"weight_unit": {
"type": "string"
}
}
}
},
"vendor": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 58 66"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><g fill-rule="nonzero" stroke="none"><path fill="#95bf47" d="M49.255 12.484a.63.63 0 0 0-.564-.527c-.225-.037-5.17-.376-5.17-.376l-3.77-3.77c-.34-.376-1.092-.266-1.376-.188-.037 0-.752.225-1.922.605-1.137-3.3-3.15-6.306-6.696-6.306h-.303C28.438.605 27.194 0 26.144 0c-8.256.037-12.2 10.333-13.434 15.594l-5.77 1.77c-1.77.564-1.835.605-2.073 2.293L0 57.175 36.468 64l19.763-4.26c0-.037-6.94-46.897-6.976-47.255zM34.431 8.86c-.917.303-1.963.605-3.1.945v-.68a15 15 0 0 0-.752-4.999c1.848.284 3.1 2.357 3.843 4.733zm-6.068-4.298c.603 1.778.883 3.65.826 5.527v.34l-6.375 1.963c1.248-4.66 3.55-6.962 5.55-7.83zm-2.45-2.293a1.94 1.94 0 0 1 1.055.339c-2.66 1.238-5.472 4.366-6.678 10.627l-5.045 1.546C16.668 10.03 19.988 2.26 25.91 2.26z"/><path fill="#5e8e3e" d="M48.691 11.957c-.225-.037-5.17-.376-5.17-.376l-3.77-3.77a.75.75 0 0 0-.527-.225L36.472 64l19.763-4.26-6.98-47.218a.68.68 0 0 0-.564-.564z"/><path d="m29.758 22.9-2.454 7.242a11.4 11.4 0 0 0-4.752-1.133c-3.848 0-4.036 2.412-4.036 3.018 0 3.298 8.636 4.564 8.636 12.333 0 6.1-3.885 10.03-9.1 10.03-6.26 0-9.467-3.885-9.467-3.885l1.665-5.55s3.28 2.83 6.073 2.83a2.47 2.47 0 0 0 2.564-2.49c0-4.34-7.1-4.527-7.1-11.618 0-5.962 4.298-11.77 12.934-11.77 3.394.05 5.018 1 5.018 1z"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB