diff --git a/packages/nodes-base/credentials/UnleashedSoftwareApi.credentials.ts b/packages/nodes-base/credentials/UnleashedSoftwareApi.credentials.ts new file mode 100644 index 0000000000..acc2e32685 --- /dev/null +++ b/packages/nodes-base/credentials/UnleashedSoftwareApi.credentials.ts @@ -0,0 +1,26 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class UnleashedSoftwareApi implements ICredentialType { + name = 'unleashedSoftwareApi'; + displayName = 'Unleashed API'; + properties = [ + { + displayName: 'API ID', + name: 'apiId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + typeOptions: { + password: true, + }, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts index 9b63a8f4b8..606867e8eb 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts @@ -459,7 +459,6 @@ export class GoogleSheet { rowData.push(''); } }); - setData.push(rowData); }); diff --git a/packages/nodes-base/nodes/Twitter/GenericFunctions.ts b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts index 1b793047c3..655345ec94 100644 --- a/packages/nodes-base/nodes/Twitter/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts @@ -48,7 +48,9 @@ export async function twitterApiRequestAllItems(this: IExecuteFunctions | ILoadO const returnData: IDataObject[] = []; let responseData; + query.count = 100; + do { responseData = await twitterApiRequest.call(this, method, endpoint, body, query); query.since_id = responseData.search_metadata.max_id; diff --git a/packages/nodes-base/nodes/UnleashedSoftware/GenericFunctions.ts b/packages/nodes-base/nodes/UnleashedSoftware/GenericFunctions.ts new file mode 100644 index 0000000000..9c4cc911eb --- /dev/null +++ b/packages/nodes-base/nodes/UnleashedSoftware/GenericFunctions.ts @@ -0,0 +1,113 @@ + +import { + OptionsWithUrl, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + } from 'n8n-workflow'; + +import { + createHmac, +} from 'crypto'; + +import * as qs from 'qs'; + +export async function unleashedApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, query: IDataObject = {} , pageNumber?: number, headers?: object): Promise { // tslint:disable-line:no-any + + const paginatedPath = pageNumber ? `/${path}/${pageNumber}` : `/${path}`; + + const options: OptionsWithUrl = { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + method, + qs: query, + body, + url: `https://api.unleashedsoftware.com/${paginatedPath}`, + json: true, + }; + + if (Object.keys(body).length === 0) { + delete options.body; + } + + const credentials = this.getCredentials('unleashedSoftwareApi'); + + if (credentials === undefined) { + + throw new Error('No credentials got returned!'); + } + + const signature = createHmac('sha256', (credentials.apiKey as string)) + .update(qs.stringify(query)) + .digest('base64'); + + options.headers = Object.assign({}, headers, { + 'api-auth-id': credentials.apiId, + 'api-auth-signature': signature, + }); + + try { + + return await this.helpers.request!(options); + + } catch (error) { + + if (error.response && error.response.body && error.response.body.description) { + + throw new Error(`Unleashed Error response [${error.statusCode}]: ${error.response.body.description}`); + } + + throw error; + } +} +export async function unleashedApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let pageNumber = 1; + + query.pageSize = 1000; + + do { + responseData = await unleashedApiRequest.call(this, method, endpoint, body, query, pageNumber); + + returnData.push.apply(returnData, responseData[propertyName]); + + pageNumber++; + + } while ( + (responseData.Pagination.PageNumber as number) < (responseData.Pagination.NumberOfPages as number) + ); + return returnData; +} + +//.NET code is serializing dates in the following format: "/Date(1586833770780)/" +//which is useless on JS side and could not treated as a date for other nodes +//so we need to convert all of the fields that has it. +export function convertNETDates(item: {[key: string]: any}){ + Object.keys(item).forEach( path => { + const type = typeof item[path] as string; + if (type === 'string') { + const value = item[path] as string; + const a = /\/Date\((\d*)\)\//.exec(value); + if (a) { + item[path] = new Date(+a[1]); + } + } if (type === 'object' && item[path]) { + convertNETDates(item[path]); + } + }); +} + diff --git a/packages/nodes-base/nodes/UnleashedSoftware/SalesOrderDescription.ts b/packages/nodes-base/nodes/UnleashedSoftware/SalesOrderDescription.ts new file mode 100644 index 0000000000..350c5e7ea8 --- /dev/null +++ b/packages/nodes-base/nodes/UnleashedSoftware/SalesOrderDescription.ts @@ -0,0 +1,191 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const salesOrderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'salesOrder', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all sales orders', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const salesOrderFields = [ + +/* ------------------------------------------------------------------------- */ +/* salesOrder:getAll */ +/* ------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'salesOrder', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'salesOrder', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Page', + name: 'page', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'salesOrder', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100000, + }, + default: 1, + description: 'Page number.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'salesOrder', + ], + }, + }, + options: [ + { + displayName: 'Customer ID', + name: 'customerId', + type: 'string', + default: '', + placeholder: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', + description: 'Only returns orders for a specified Customer GUID. The CustomerId can be specified as a list of comma-separated GUIDs' + }, + { + displayName: 'Customer Code', + name: 'customerCode', + type: 'string', + default: '', + description: 'Returns orders that start with the specific customer code.' + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + description: 'Returns orders with order date before the specified date. UTC.' + }, + { + displayName: 'Modified Since', + name: 'modifiedSince', + type: 'dateTime', + default: '', + description: 'Returns orders created or edited after a specified date, must be UTC format.' + }, + { + displayName: 'Order Number', + name: 'orderNumber', + type: 'string', + default: '', + description: 'Returns a single order with the specified order number. If set, it overrides all other filters.' + }, + { + displayName: 'Order Status', + name: 'orderStatus', + type: 'multiOptions', + options: [ + { + name: 'Completed', + value: 'Completed' + }, + { + name: 'Backordered', + value: 'Backordered' + }, + { + name: 'Parked', + value: 'Parked' + }, + { + name: 'Placed', + value: 'Placed' + }, + { + name: 'Deleted', + value: 'Deleted' + } + ], + default: [], + required: false, + description: 'Returns orders with the specified status. If no orderStatus filter is specified, then we exclude “Deleted” by default.' + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + description: 'Returns orders with order date after the specified date. UTC.' + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/UnleashedSoftware/StockOnHandDescription.ts b/packages/nodes-base/nodes/UnleashedSoftware/StockOnHandDescription.ts new file mode 100644 index 0000000000..3853f36eca --- /dev/null +++ b/packages/nodes-base/nodes/UnleashedSoftware/StockOnHandDescription.ts @@ -0,0 +1,192 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const stockOnHandOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'stockOnHand', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a stock on hand', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all stocks on hand', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const stockOnHandFields = [ + + +/* ------------------------------------------------------------------------- */ +/* stockOnHand:get */ +/* ------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'stockOnHand', + ], + }, + }, + default: '', + }, +/* ------------------------------------------------------------------------- */ +/* stockOnHand:getAll */ +/* ------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'stockOnHand', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'stockOnHand', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Page', + name: 'page', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'stockOnHand', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100000, + }, + default: 1, + description: 'Page number.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'stockOnHand', + ], + }, + }, + options: [ + { + displayName: 'As at Date', + name: 'asAtDate', + type: 'dateTime', + default: '', + description: 'Returns the stock on hand for a specific date.' + }, + { + displayName: 'Is Assembled', + name: 'IsAssembled', + type: 'boolean', + default: '', + description: 'If set to True, the AvailableQty will also include the quantity that can be assembled.' + }, + { + displayName: 'Modified Since', + name: 'modifiedSince', + type: 'dateTime', + default: '', + description: 'Returns stock on hand values modified after a specific date.' + }, + { + displayName: 'Order By', + name: 'orderBy', + type: 'string', + default: '', + description: 'Orders the list by a specific column, by default the list is ordered by productCode.' + }, + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + default: '', + description: 'Returns products with the specific Product Guid. You can enter multiple product Ids separated by commas.' + }, + { + displayName: 'Warehouse Code', + name: 'warehouseCode', + type: 'string', + default: '', + description: 'Returns stock on hand for a specific warehouse code.' + }, + { + displayName: 'Warehouse Name', + name: 'warehouseName', + type: 'string', + default: '', + description: 'Returns stock on hand for a specific warehouse name.' + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/UnleashedSoftware/UnleashedSoftware.node.ts b/packages/nodes-base/nodes/UnleashedSoftware/UnleashedSoftware.node.ts new file mode 100644 index 0000000000..b3a79926d7 --- /dev/null +++ b/packages/nodes-base/nodes/UnleashedSoftware/UnleashedSoftware.node.ts @@ -0,0 +1,219 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + unleashedApiRequest, + unleashedApiRequestAllItems, + convertNETDates, +} from './GenericFunctions'; + +import { + salesOrderOperations, + salesOrderFields, +} from './SalesOrderDescription'; + +import { + stockOnHandOperations, + stockOnHandFields, +} from './StockOnHandDescription'; + +import * as moment from 'moment'; + +export class UnleashedSoftware implements INodeType { + description: INodeTypeDescription = { + displayName: 'Unleashed Software', + name: 'unleashedSoftware', + group: ['transform'], + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + icon: 'file:unleashedSoftware.png', + version: 1, + description: 'Consume Unleashed Software API', + defaults: { + name: 'Unleashed Software', + color: '#772244', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'unleashedSoftwareApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Sales Order', + value: 'salesOrder', + }, + { + name: 'Stock On Hand', + value: 'stockOnHand', + }, + ], + default: 'salesOrder', + description: 'The resource to operate on.', + }, + ...salesOrderOperations, + ...salesOrderFields, + + ...stockOnHandOperations, + ...stockOnHandFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); + + const returnData: IDataObject[] = []; + + const length = items.length; + + const qs: IDataObject = {}; + + let responseData; + + for (let i = 0; i < length; i++) { + + const resource = this.getNodeParameter('resource', 0) as string; + + const operation = this.getNodeParameter('operation', 0) as string; + + //https://apidocs.unleashedsoftware.com/SalesOrders + if (resource === 'salesOrder') { + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (filters.startDate) { + + filters.startDate = moment(filters.startDate as string).format('YYYY-MM-DD'); + } + + if (filters.endDate) { + + filters.endDate = moment(filters.endDate as string).format('YYYY-MM-DD'); + } + + if (filters.modifiedSince) { + + filters.modifiedSince = moment(filters.modifiedSince as string).format('YYYY-MM-DD'); + } + + if (filters.orderStatus) { + + filters.orderStatus = (filters.orderStatus as string[]).join(','); + } + + Object.assign(qs, filters); + + if (returnAll) { + + responseData = await unleashedApiRequestAllItems.call(this, 'Items', 'GET', '/SalesOrders', {}, qs); + + } else { + + const limit = this.getNodeParameter('limit', i) as number; + + qs.pageSize = limit; + + const pageNumber = this.getNodeParameter('page', i) as number; + + responseData = await unleashedApiRequest.call(this, 'GET', `/SalesOrders`, {}, qs, pageNumber); + + responseData = responseData.Items; + } + + convertNETDates(responseData); + } + } + + //https://apidocs.unleashedsoftware.com/StockOnHand + if (resource === 'stockOnHand') { + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (filters.asAtDate) { + + filters.asAtDate = moment(filters.asAtDate as string).format('YYYY-MM-DD'); + } + + if (filters.modifiedSince) { + + filters.modifiedSince = moment(filters.modifiedSince as string).format('YYYY-MM-DD'); + } + + if (filters.orderBy) { + + filters.orderBy = (filters.orderBy as string).trim(); + } + + Object.assign(qs, filters); + + if (returnAll) { + + responseData = await unleashedApiRequestAllItems.call(this, 'Items', 'GET', '/StockOnHand', {}, qs); + + } else { + + const limit = this.getNodeParameter('limit', i) as number; + + qs.pageSize = limit; + + const pageNumber = this.getNodeParameter('page', i) as number; + + responseData = await unleashedApiRequest.call(this, 'GET', `/StockOnHand`, {}, qs, pageNumber); + + responseData = responseData.Items; + } + + convertNETDates(responseData); + } + + if (operation === 'get') { + + const productId = this.getNodeParameter('productId', i) as string; + + responseData = await unleashedApiRequest.call(this, 'GET', `/StockOnHand/${productId}`); + + convertNETDates(responseData); + } + } + + if (Array.isArray(responseData)) { + + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else { + + returnData.push(responseData as IDataObject); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} + + + + diff --git a/packages/nodes-base/nodes/UnleashedSoftware/unleashedSoftware.png b/packages/nodes-base/nodes/UnleashedSoftware/unleashedSoftware.png new file mode 100644 index 0000000000..d6afb1aff8 Binary files /dev/null and b/packages/nodes-base/nodes/UnleashedSoftware/unleashedSoftware.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ddceee6190..d2f3ecfb0d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -154,6 +154,7 @@ "dist/credentials/TogglApi.credentials.js", "dist/credentials/TwakeCloudApi.credentials.js", "dist/credentials/TwakeServerApi.credentials.js", + "dist/credentials/UnleashedSoftwareApi.credentials.js", "dist/credentials/UpleadApi.credentials.js", "dist/credentials/VeroApi.credentials.js", "dist/credentials/WebflowApi.credentials.js", @@ -325,6 +326,7 @@ "dist/nodes/Twitter/Twitter.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js", "dist/nodes/Twake/Twake.node.js", + "dist/nodes/UnleashedSoftware/UnleashedSoftware.node.js", "dist/nodes/Uplead/Uplead.node.js", "dist/nodes/Vero/Vero.node.js", "dist/nodes/Webflow/WebflowTrigger.node.js",