diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts index 750b6f4529..ead7024fa5 100644 --- a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts @@ -34,6 +34,9 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions body, json: true }; + + console.log(options); + if (Object.keys(option).length !== 0) { Object.assign(options, option); } diff --git a/packages/nodes-base/nodes/Shopify/ProductDescription.ts b/packages/nodes-base/nodes/Shopify/ProductDescription.ts new file mode 100644 index 0000000000..008e66ce08 --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/ProductDescription.ts @@ -0,0 +1,799 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const productOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'product', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a product', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a product', + }, + { + name: 'Get', + value: 'get', + description: 'Get a product', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all products', + }, + { + name: 'Update', + value: 'update', + description: 'Update a product', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const productFields = [ + + /* -------------------------------------------------------------------------- */ + /* 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: `Specifies the location of the product image.
+ This parameter supports URL filters that you can use to retrieve modified copies of the image.
+ 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).`, + }, + { + 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: `Specifies the location of the product image.
+ This parameter supports URL filters that you can use to retrieve modified copies of the image.
+ 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).`, + }, + { + 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: 'If all results should be returned 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: 'How many 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.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Shopify/ProductInterface.ts b/packages/nodes-base/nodes/Shopify/ProductInterface.ts new file mode 100644 index 0000000000..a19748a0a0 --- /dev/null +++ b/packages/nodes-base/nodes/Shopify/ProductInterface.ts @@ -0,0 +1,66 @@ +import { + 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; +} diff --git a/packages/nodes-base/nodes/Shopify/Shopify.node.ts b/packages/nodes-base/nodes/Shopify/Shopify.node.ts index 45f0d906b1..dc093bb129 100644 --- a/packages/nodes-base/nodes/Shopify/Shopify.node.ts +++ b/packages/nodes-base/nodes/Shopify/Shopify.node.ts @@ -22,6 +22,11 @@ import { orderOperations, } from './OrderDescription'; +import { + productFields, + productOperations, +} from './ProductDescription'; + import { IOrder, IDiscountCode, @@ -29,6 +34,10 @@ import { ILineItem, } from './OrderInterface'; +import { + IProduct, +} from './ProductInterface'; + export class Shopify implements INodeType { description: INodeTypeDescription = { displayName: 'Shopify', @@ -48,7 +57,7 @@ export class Shopify implements INodeType { { name: 'shopifyApi', required: true, - } + }, ], properties: [ { @@ -60,6 +69,10 @@ export class Shopify implements INodeType { name: 'Order', value: 'order', }, + { + name: 'Product', + value: 'product', + }, ], default: 'order', description: 'Resource to consume.', @@ -67,6 +80,9 @@ export class Shopify implements INodeType { // ORDER ...orderOperations, ...orderFields, + // PRODUCTS + ...productOperations, + ...productFields, ], }; @@ -269,6 +285,80 @@ export class Shopify implements INodeType { 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, {}) as IDataObject; + + 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, {}) as IDataObject; + 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, {}) as IDataObject; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + Object.assign(qs, additionalFields); + + if (returnAll === true) { + responseData = await shopifyApiRequestAllItems.call(this, 'products', 'GET', '/products.json', {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + 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, {}) as IDataObject; + + 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; + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]);