diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts index d7e0634d0c..750b6f4529 100644 --- a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts @@ -1,4 +1,6 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, + } from 'request'; import { IExecuteFunctions, @@ -12,7 +14,11 @@ import { IDataObject, } from 'n8n-workflow'; -export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any +import { + snakeCase, +} from 'change-case'; + +export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('shopifyApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); @@ -28,15 +34,71 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions body, json: true }; - + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(query).length === 0) { + delete options.qs; + } try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message; - - if (errorMessage !== undefined) { - throw errorMessage; + if (error.response.body && error.response.body.errors) { + let message = ''; + if (typeof error.response.body.errors === 'object') { + for (const key of Object.keys(error.response.body.errors)) { + message+= error.response.body.errors[key]; + } + } else { + message = `${error.response.body.errors} |`; + } + const errorMessage = `Shopify error response [${error.statusCode}]: ${message}`; + throw new Error(errorMessage); } - throw error.response.body; + + throw error; } } + + +export async function shopifyApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + 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]); + } while ( + responseData.headers['link'] !== undefined && + 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; +} diff --git a/packages/nodes-base/nodes/Shopify/OrderDescription.ts b/packages/nodes-base/nodes/Shopify/OrderDescription.ts new file mode 100644 index 0000000000..118157ba23 --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/OrderDescription.ts @@ -0,0 +1,952 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const orderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'order', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an order', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an order', + }, + { + name: 'Get', + value: 'get', + description: 'Get an order', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all orders', + }, + { + name: 'Update', + value: 'update', + description: 'Update an order', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const orderFields = [ + +/* -------------------------------------------------------------------------- */ +/* 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, + }, + description: 'Billing address', + 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', + 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 ID', + name: 'locationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLocations', + }, + default: '', + description: 'The ID of the physical location where the order was processed.', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + 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, + }, + description: 'Shipping Address', + 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: '', + 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 ID', + name: 'productId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProducts', + }, + default: '', + description: 'The ID of the product that the line item belongs to', + }, + { + 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: 'If all results should be returned 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: 'How many 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: 'Voided', + value: 'voided', + description: 'Show only voided orders', + }, + { + name: 'Unpaid', + value: 'unpaid', + description: 'Show authorized and partially paid 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: 'Unshipped', + value: 'unshipped', + description: 'Show orders that have not yet been shipped. Returns orders with fulfillment_status of null.', + }, + { + name: 'Unfulfilled', + value: 'unfulfilled', + description: 'Returns orders with fulfillment_status of null or partial.', + }, + ], + 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', + default: '', + description: `The customer's email address.`, + }, + { + displayName: 'Location ID', + name: 'locationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLocations', + }, + default: '', + description: 'The ID of the physical location where the order was processed.', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + 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, + }, + description: 'Shipping Address', + 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.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Shopify/OrderInterface.ts b/packages/nodes-base/nodes/Shopify/OrderInterface.ts new file mode 100644 index 0000000000..42f9398215 --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/OrderInterface.ts @@ -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; +} diff --git a/packages/nodes-base/nodes/Shopify/Shopify.node.ts b/packages/nodes-base/nodes/Shopify/Shopify.node.ts new file mode 100644 index 0000000000..d309dbf60b --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/Shopify.node.ts @@ -0,0 +1,281 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeTypeDescription, + INodeExecutionData, + INodeType, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + keysToSnakeCase, + shopifyApiRequest, + shopifyApiRequestAllItems, +} from './GenericFunctions'; + +import { + orderFields, + orderOperations, +} from './OrderDescription'; + +import { + IOrder, + IDiscountCode, + IAddress, + ILineItem, +} from './OrderInterface'; + +export class Shopify implements INodeType { + description: INodeTypeDescription = { + displayName: 'Shopify', + name: 'shopifyN', + icon: 'file:shopify.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Shopify API', + defaults: { + name: 'Shopify', + color: '#559922', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'shopifyApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Order', + value: 'order', + }, + ], + default: 'order', + description: 'Resource to consume.', + }, + // ORDER + ...orderOperations, + ...orderFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available products to display them to user so that he can + // select them easily + async getProducts(this: ILoadOptionsFunctions): Promise { + 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 he can + // select them easily + async getLocations(this: ILoadOptionsFunctions): Promise { + 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 { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: IDataObject = {}; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + 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) as IDataObject; + 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 Error('At least one line item has to be added'); + } + 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) as IDataObject; + 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) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + 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 === true) { + responseData = await shopifyApiRequestAllItems.call(this, 'orders', 'GET', '/orders.json', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + 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) as IDataObject; + 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; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts b/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts index bd820833ee..9e8ec2c811 100644 --- a/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts @@ -14,7 +14,9 @@ import { shopifyApiRequest, } from './GenericFunctions'; -import { createHmac } from 'crypto'; +import { + createHmac, + } from 'crypto'; export class ShopifyTrigger implements INodeType { description: INodeTypeDescription = { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c6d643d85b..127f41ab15 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -213,6 +213,7 @@ "dist/nodes/RssFeedRead.node.js", "dist/nodes/Rundeck/Rundeck.node.js", "dist/nodes/Set.node.js", + "dist/nodes/Shopify/Shopify.node.js", "dist/nodes/Shopify/ShopifyTrigger.node.js", "dist/nodes/Slack/Slack.node.js", "dist/nodes/SplitInBatches.node.js",