├── .nvmrc
├── nodes
└── NetSuite
│ ├── types.d.ts
│ ├── netSuite.svg
│ ├── NetSuite.node.json
│ ├── NetSuite.node.types.ts
│ ├── NetSuite.node.options.ts
│ └── NetSuite.node.ts
├── .gitignore
├── gulpfile.js
├── .editorconfig
├── .eslintrc.js
├── tsconfig.json
├── LICENSE.md
├── credentials
└── NetSuite.credentials.ts
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/nodes/NetSuite/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@fye/netsuite-rest-api';
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | .tmp
4 | tmp
5 | dist
6 | npm-debug.log*
7 | package-lock.json
8 | yarn.lock
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const { src, dest } = require('gulp');
2 |
3 | function copyIcons() {
4 | src('nodes/**/*.{png,svg}')
5 | .pipe(dest('dist/nodes'))
6 |
7 | return src('credentials/**/*.{png,svg}')
8 | .pipe(dest('dist/credentials'));
9 | }
10 |
11 | exports.default = copyIcons;
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = tab
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [package.json]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.yml]
16 | indent_style = space
17 | indent_size = 2
18 |
--------------------------------------------------------------------------------
/nodes/NetSuite/netSuite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/NetSuite/NetSuite.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "node": "n8n-nodes-netsuite.netsuite",
3 | "nodeVersion": "1.0",
4 | "codexVersion": "1.0",
5 | "categories": [
6 | "Finance & Accounting",
7 | "Sales"
8 | ],
9 | "resources": {
10 | "credentialDocumentation": [
11 | {
12 | "url": "https://github.com/drudge/n8n-nodes-netsuite"
13 | }
14 | ],
15 | "primaryDocumentation": [
16 | {
17 | "url": "https://github.com/drudge/n8n-nodes-netsuite"
18 | }
19 | ]
20 | }
21 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@types/eslint').ESLint.ConfigData}
3 | */
4 | module.exports = {
5 | root: true,
6 |
7 | env: {
8 | browser: true,
9 | es6: true,
10 | node: true,
11 | },
12 |
13 | parser: '@typescript-eslint/parser',
14 |
15 | parserOptions: {
16 | project: ['./tsconfig.json'],
17 | sourceType: 'module',
18 | extraFileExtensions: ['.json'],
19 | },
20 |
21 | ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'],
22 |
23 | overrides: [
24 | {
25 | files: ['package.json', 'credentials/**/*.ts', 'nodes/**/*.ts']
26 | },
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "target": "es2019",
7 | "lib": ["es2019", "es2020"],
8 | "removeComments": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noImplicitAny": true,
11 | "noImplicitReturns": true,
12 | "noUnusedLocals": true,
13 | "strictNullChecks": true,
14 | "preserveConstEnums": true,
15 | "esModuleInterop": true,
16 | "resolveJsonModule": true,
17 | "incremental": true,
18 | "declaration": true,
19 | "sourceMap": true,
20 | "skipLibCheck": true,
21 | "outDir": "./dist/",
22 | },
23 | "include": [
24 | "credentials/**/*",
25 | "nodes/**/*",
26 | "nodes/**/*.json",
27 | "package.json",
28 | ],
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Nicholas Penree
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/credentials/NetSuite.credentials.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ICredentialType,
3 | INodeProperties,
4 | } from 'n8n-workflow';
5 |
6 | export class NetSuite implements ICredentialType {
7 | name = 'netsuite';
8 | displayName = 'NetSuite';
9 | documentationUrl = 'netsuite';
10 | properties: INodeProperties[] = [
11 | {
12 | displayName: 'Hostname',
13 | name: 'hostname',
14 | type: 'string',
15 | default: '',
16 | required: true,
17 | },
18 | {
19 | displayName: 'Account ID',
20 | name: 'accountId',
21 | type: 'string',
22 | default: '',
23 | required: true,
24 | description: 'NetSuite Account ID',
25 | },
26 | {
27 | displayName: 'Consumer Key',
28 | name: 'consumerKey',
29 | type: 'string',
30 | default: '',
31 | required: true,
32 | },
33 | {
34 | displayName: 'Consumer Secret',
35 | name: 'consumerSecret',
36 | type: 'string',
37 | typeOptions: {
38 | password: true,
39 | },
40 | default: '',
41 | required: true,
42 | },
43 | {
44 | displayName: 'Token Key',
45 | name: 'tokenKey',
46 | type: 'string',
47 | default: '',
48 | required: true,
49 | },
50 | {
51 | displayName: 'Token Secret',
52 | name: 'tokenSecret',
53 | type: 'string',
54 | typeOptions: {
55 | password: true,
56 | },
57 | default: '',
58 | required: true,
59 | },
60 | ];
61 | }
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "n8n-nodes-netsuite",
3 | "version": "0.7.5",
4 | "description": "n8n node for NetSuite using the REST API",
5 | "license": "MIT",
6 | "homepage": "https://github.com/drudge/n8n-nodes-netsuite",
7 | "engines": {
8 | "node": ">=18.17"
9 | },
10 | "author": {
11 | "name": "Nicholas Penree",
12 | "email": "nick@penree.com"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/drudge/n8n-nodes-netsuite.git"
17 | },
18 | "main": "index.js",
19 | "scripts": {
20 | "dev": "tsc --watch",
21 | "build": "tsc && gulp",
22 | "lint": "eslint nodes credentials package.json",
23 | "lintfix": "eslint nodes credentials package.json --fix",
24 | "prepublishOnly": "npm run build && npm run lint"
25 | },
26 | "files": [
27 | "dist"
28 | ],
29 | "keywords": [
30 | "n8n",
31 | "node",
32 | "netsuite",
33 | "rest",
34 | "api",
35 | "suitetalk",
36 | "n8n-node",
37 | "n8n-community-node-package"
38 | ],
39 | "n8n": {
40 | "n8nNodesApiVersion": 1,
41 | "credentials": [
42 | "dist/credentials/NetSuite.credentials.js"
43 | ],
44 | "nodes": [
45 | "dist/nodes/NetSuite/NetSuite.node.js"
46 | ]
47 | },
48 | "devDependencies": {
49 | "@types/node": "^18.19.39",
50 | "@typescript-eslint/parser": "^7.16.0",
51 | "eslint": "^8.57.0",
52 | "gulp": "^5.0.0",
53 | "n8n-workflow": "*",
54 | "typescript": "^5.5.3"
55 | },
56 | "peerDependencies": {
57 | "n8n-workflow": "*"
58 | },
59 | "dependencies": {
60 | "@fye/netsuite-rest-api": "^2.3.1",
61 | "p-limit": "^3.1.0"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/nodes/NetSuite/NetSuite.node.types.ts:
--------------------------------------------------------------------------------
1 | import { IExecuteFunctions, INodeExecutionData, JsonObject } from 'n8n-workflow';
2 |
3 | export type INetSuiteCredentials = {
4 | hostname: string;
5 | accountId: string;
6 | consumerKey: string;
7 | consumerSecret: string;
8 | tokenKey: string;
9 | tokenSecret: string;
10 | netsuiteQueryLimit?: number;
11 | };
12 |
13 | export type INetSuiteOperationOptions = {
14 | item?: INodeExecutionData;
15 | fns: IExecuteFunctions;
16 | credentials: INetSuiteCredentials;
17 | itemIndex: number;
18 | };
19 |
20 | export enum NetSuiteRequestType {
21 | Record = 'record',
22 | SuiteQL = 'suiteql',
23 | Workbook = 'workbook',
24 | }
25 | export type INetSuiteRequestOptions = {
26 | nextUrl?: string;
27 | method: string;
28 | body?: any;
29 | headers?: any;
30 | query?: any;
31 | path?: string;
32 | requestType: NetSuiteRequestType;
33 | };
34 |
35 | export type INetSuiteResponse = {
36 | statusCode: number;
37 | statusText: string;
38 | body: INetSuiteResponseBody;
39 | headers: any;
40 | request: any;
41 | };
42 |
43 | export type INetSuiteResponseBody = {
44 | statusCode: number;
45 | title?: string;
46 | code?: string;
47 | body: JsonObject;
48 | 'o:errorCode'?: string;
49 | 'o:errorDetails'?: INetSuiteErrorDetails[];
50 | message?: string;
51 | };
52 |
53 | export type INetSuiteErrorDetails = {
54 | detail: string;
55 | };
56 |
57 | export type INetSuitePagedBody = {
58 | hasMore: boolean;
59 | items: JsonObject[];
60 | nextUrl?: string;
61 | links: INetSuiteLink[];
62 | count?: number;
63 | totalResults?: number;
64 | offset?: number;
65 | };
66 |
67 | export type INetSuiteLink = {
68 | rel: string;
69 | href: string;
70 | };
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # n8n-nodes-netsuite
2 |
3 | 
4 |
5 | n8n node for interacting with NetSuite using [SuiteTalk REST Web Services](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_1540391670.html).
6 |
7 | ## How to install
8 |
9 | ### Community Nodes (Recommended)
10 |
11 | For users on n8n v0.187+, your instance owner can install this node from [Community Nodes](https://docs.n8n.io/integrations/community-nodes/installation/).
12 |
13 | 1. Go to **Settings > Community Nodes**.
14 | 2. Select **Install**.
15 | 3. Enter `n8n-nodes-netsuite` in **Enter npm package name**.
16 | 4. Agree to the [risks](https://docs.n8n.io/integrations/community-nodes/risks/) of using community nodes: select **I understand the risks of installing unverified code from a public source**.
17 | 5. Select **Install**.
18 |
19 | After installing the node, you can use it like any other node. n8n displays the node in search results in the **Nodes** panel.
20 |
21 | ### Manual installation
22 |
23 | To get started install the package in your n8n root directory:
24 |
25 | `npm install n8n-nodes-netsuite`
26 |
27 |
28 | For Docker-based deployments, add the following line before the font installation command in your [n8n Dockerfile](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/Dockerfile):
29 |
30 |
31 | `RUN cd /usr/local/lib/node_modules/n8n && npm install n8n-nodes-netsuite`
32 |
33 | ## License
34 |
35 | MIT License
36 |
37 | Copyright (c) 2022 Nicholas Penree
38 |
39 | Permission is hereby granted, free of charge, to any person obtaining a copy
40 | of this software and associated documentation files (the "Software"), to deal
41 | in the Software without restriction, including without limitation the rights
42 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
43 | copies of the Software, and to permit persons to whom the Software is
44 | furnished to do so, subject to the following conditions:
45 |
46 | The above copyright notice and this permission notice shall be included in all
47 | copies or substantial portions of the Software.
48 |
49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
51 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
52 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
53 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
54 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
55 | SOFTWARE.
56 |
--------------------------------------------------------------------------------
/nodes/NetSuite/NetSuite.node.options.ts:
--------------------------------------------------------------------------------
1 | import {
2 | INodeTypeDescription,
3 | } from 'n8n-workflow';
4 |
5 | /**
6 | * Options to be displayed
7 | */
8 | export const nodeDescription: INodeTypeDescription = {
9 | displayName: 'NetSuite',
10 | name: 'netsuite',
11 | group: ['input'],
12 | version: 1,
13 | description: 'NetSuite REST API',
14 | defaults: {
15 | name: 'NetSuite',
16 | color: '#125580',
17 | },
18 | icon: 'file:netSuite.svg',
19 | inputs: ['main'],
20 | outputs: ['main'],
21 | credentials: [
22 | {
23 | name: 'netsuite',
24 | required: true,
25 | },
26 | ],
27 | properties: [
28 | {
29 | displayName: 'Operation',
30 | name: 'operation',
31 | type: 'options',
32 | options: [
33 | {
34 | name: 'List Records',
35 | value: 'listRecords',
36 | },
37 | {
38 | name: 'Get Record',
39 | value: 'getRecord',
40 | },
41 | {
42 | name: 'Insert Record',
43 | value: 'insertRecord',
44 | },
45 | {
46 | name: 'Update Record',
47 | value: 'updateRecord',
48 | },
49 | {
50 | name: 'Remove Record',
51 | value: 'removeRecord',
52 | },
53 | {
54 | name: 'Execute SuiteQL',
55 | value: 'runSuiteQL',
56 | },
57 | // {
58 | // name: 'Get Workbook',
59 | // value: 'getWorkbook',
60 | // },
61 | {
62 | name: 'Raw Request',
63 | value: 'rawRequest',
64 | },
65 | ],
66 | default: 'getRecord',
67 | },
68 | {
69 | displayName: 'Request Type',
70 | name: 'requestType',
71 | type: 'options',
72 | options: [
73 | {
74 | name: 'Record',
75 | value: 'record',
76 | },
77 | {
78 | name: 'SuiteQL',
79 | value: 'suiteql',
80 | },
81 | {
82 | name: 'Workbook',
83 | value: 'workbook',
84 | },
85 | {
86 | name: 'Dataset',
87 | value: 'dataset',
88 | },
89 | ],
90 | displayOptions: {
91 | show: {
92 | operation: [
93 | 'rawRequest',
94 | ],
95 | },
96 | },
97 | required: true,
98 | default: 'record',
99 | },
100 | {
101 | displayName: 'HTTP Method',
102 | name: 'method',
103 | type: 'options',
104 | options: [
105 | {
106 | name: 'DELETE',
107 | value: 'DELETE',
108 | },
109 | {
110 | name: 'GET',
111 | value: 'GET',
112 | },
113 | {
114 | name: 'HEAD',
115 | value: 'HEAD',
116 | },
117 | {
118 | name: 'OPTIONS',
119 | value: 'OPTIONS',
120 | },
121 | {
122 | name: 'PATCH',
123 | value: 'PATCH',
124 | },
125 | {
126 | name: 'POST',
127 | value: 'POST',
128 | },
129 | {
130 | name: 'PUT',
131 | value: 'PUT',
132 | },
133 | ],
134 | default: 'GET',
135 | description: 'The request method to use.',
136 | displayOptions: {
137 | show: {
138 | operation: [
139 | 'rawRequest',
140 | ],
141 | },
142 | },
143 | required: true,
144 | },
145 | {
146 | displayName: 'Path',
147 | name: 'path',
148 | type: 'string',
149 | required: true,
150 | default: 'services/rest/record/v1/salesOrder',
151 | displayOptions: {
152 | show: {
153 | operation: [
154 | 'rawRequest',
155 | ],
156 | },
157 | },
158 | },
159 | {
160 | displayName: 'Record Type',
161 | name: 'recordType',
162 | type: 'options',
163 | options: [
164 | { name: 'Assembly Item', value: 'assemblyItem' },
165 | { name: 'Billing Account', value: 'billingAccount' },
166 | { name: 'Calendar Event', value: 'calendarEvent' },
167 | { name: 'Cash Sale', value: 'cashSale' },
168 | { name: 'Charge', value: 'charge' },
169 | { name: 'Classification (BETA)', value: 'classification' },
170 | { name: 'Contact', value: 'contact' },
171 | { name: 'Contact Category', value: 'contactCategory' },
172 | { name: 'Contact Role', value: 'contactRole' },
173 | { name: 'Credit Memo', value: 'creditMemo' },
174 | { name: 'Custom Record (*)', value: 'custom' },
175 | { name: 'Customer', value: 'customer' },
176 | { name: 'Customer Subsidiary Relationship', value: 'customerSubsidiaryRelationship' },
177 | { name: 'Email Template', value: 'emailTemplate' },
178 | { name: 'Employee', value: 'employee' },
179 | { name: 'Inventory Item', value: 'inventoryItem' },
180 | { name: 'Invoice', value: 'invoice' },
181 | { name: 'Item Fulfillment', value: 'itemFulfillment' },
182 | { name: 'Journal Entry', value: 'journalEntry' },
183 | { name: 'Message', value: 'message' },
184 | { name: 'Non-Inventory Sale Item', value: 'nonInventorySaleItem' },
185 | { name: 'Opportunity (BETA)', value: 'opportunity'},
186 | { name: 'Phone Call', value: 'phoneCall' },
187 | { name: 'Price Book', value: 'priceBook' },
188 | { name: 'Price Plan', value: 'pricePlan' },
189 | { name: 'Purchase Order', value: 'purchaseOrder' },
190 | { name: 'Sales Order', value: 'salesOrder' },
191 | { name: 'Subscription', value: 'subscription' },
192 | { name: 'Subscription Change Order', value: 'subscriptionChangeOrder' },
193 | { name: 'Subscription Line', value: 'subscriptionLine' },
194 | { name: 'Subscription Plan', value: 'subscriptionPlan' },
195 | { name: 'Subscription Term', value: 'subscriptionTerm' },
196 | { name: 'Subsidiary', value: 'subsidiary' },
197 | { name: 'Task', value: 'task' },
198 | { name: 'Time Bill', value: 'timeBill' },
199 | { name: 'Usage', value: 'usage' },
200 | { name: 'Vendor', value: 'vendor' },
201 | { name: 'Vendor Bill', value: 'vendorBill' },
202 | { name: 'Vendor Subsidiary Relationship', value: 'vendorSubsidiaryRelationship' },
203 | ],
204 | displayOptions: {
205 | show: {
206 | operation: [
207 | 'getRecord',
208 | 'updateRecord',
209 | 'removeRecord',
210 | 'listRecords',
211 | 'insertRecord',
212 | ],
213 | },
214 | },
215 | default: 'salesOrder',
216 | },
217 | {
218 | displayName: 'Custom Record Script ID',
219 | name: 'customRecordTypeScriptId',
220 | type: 'string',
221 | required: true,
222 | default: '',
223 | displayOptions: {
224 | show: {
225 | operation: [
226 | 'getRecord',
227 | 'updateRecord',
228 | 'removeRecord',
229 | 'listRecords',
230 | 'insertRecord',
231 | ],
232 | recordType: [
233 | 'custom',
234 | ],
235 | },
236 | },
237 | description: 'The internal identifier of the Custom Record type. These normally start with customrecord',
238 | },
239 | {
240 | displayName: 'ID',
241 | name: 'internalId',
242 | type: 'string',
243 | required: true,
244 | default: '',
245 | displayOptions: {
246 | show: {
247 | operation: [
248 | 'getRecord',
249 | 'updateRecord',
250 | 'removeRecord',
251 | ],
252 | },
253 | },
254 | description: 'The internal identifier of the record. Prefix with eid: to use the external identifier.',
255 | },
256 | {
257 | displayName: 'Query',
258 | name: 'query',
259 | type: 'string',
260 | required: false,
261 | default: '',
262 | displayOptions: {
263 | show: {
264 | operation: [
265 | 'listRecords',
266 | 'runSuiteQL',
267 | ],
268 | },
269 | },
270 | },
271 | {
272 | displayName: 'Body',
273 | name: 'body',
274 | type: 'string',
275 | required: false,
276 | default: '',
277 | displayOptions: {
278 | show: {
279 | operation: [
280 | 'rawRequest',
281 | ],
282 | },
283 | },
284 | },
285 | {
286 | displayName: 'Restrict Returned Fields',
287 | name: 'fields',
288 | type: 'string',
289 | required: false,
290 | default: '',
291 | displayOptions: {
292 | show: {
293 | operation: [
294 | 'getRecord',
295 | ],
296 | },
297 | },
298 | description: 'Optionally return only the specified fields and sublists in the response.',
299 | },
300 | {
301 | displayName: 'Replace Sublists',
302 | name: 'replace',
303 | type: 'string',
304 | required: false,
305 | default: '',
306 | displayOptions: {
307 | show: {
308 | operation: [
309 | 'insertRecord',
310 | 'updateRecord',
311 | ],
312 | },
313 | },
314 | description: 'The names of sublists on this record. All sublist lines will be replaced with lines specified in the request. The sublists not specified here will have lines added to the record. The names are delimited by comma.',
315 | },
316 | {
317 | displayName: 'Replace Selected Fields',
318 | name: 'replaceSelectedFields',
319 | type: 'boolean',
320 | required: true,
321 | default: false,
322 | displayOptions: {
323 | show: {
324 | operation: [
325 | 'updateRecord',
326 | ],
327 | },
328 | },
329 | description: 'If true, all fields that should be deleted in the update request, including body fields, must be included in the replace query parameter.',
330 | },
331 | {
332 | displayName: 'Expand Sub-resources',
333 | name: 'expandSubResources',
334 | type: 'boolean',
335 | required: true,
336 | default: false,
337 | displayOptions: {
338 | show: {
339 | operation: [
340 | 'getRecord',
341 | ],
342 | },
343 | },
344 | description: 'If true, automatically expands all sublists, sublist lines, and subrecords on this record.',
345 | },
346 | {
347 | displayName: 'Simple Enum Format',
348 | name: 'simpleEnumFormat',
349 | type: 'boolean',
350 | required: true,
351 | default: false,
352 | displayOptions: {
353 | show: {
354 | operation: [
355 | 'getRecord',
356 | ],
357 | },
358 | },
359 | description: 'If true, returns enumeration values in a format that only shows the internal ID value.',
360 | },
361 | {
362 | displayName: 'Return All',
363 | name: 'returnAll',
364 | type: 'boolean',
365 | displayOptions: {
366 | show: {
367 | operation: [
368 | 'listRecords',
369 | 'runSuiteQL',
370 | ],
371 | },
372 | },
373 | default: false,
374 | description: 'Whether all results should be returned or only up to a given limit',
375 | },
376 | {
377 | displayName: 'Limit',
378 | name: 'limit',
379 | type: 'number',
380 | displayOptions: {
381 | show: {
382 | operation: [
383 | 'listRecords',
384 | 'runSuiteQL',
385 | ],
386 | returnAll: [
387 | false,
388 | ],
389 | },
390 | },
391 | typeOptions: {
392 | minValue: 1,
393 | maxValue: 1000,
394 | },
395 | default: 100,
396 | description: 'How many records to return',
397 | },
398 | {
399 | displayName: 'Offset',
400 | name: 'offset',
401 | type: 'number',
402 | displayOptions: {
403 | show: {
404 | operation: [
405 | 'listRecords',
406 | 'runSuiteQL',
407 | ],
408 | returnAll: [
409 | false,
410 | ],
411 | },
412 | },
413 | typeOptions: {
414 | minValue: 0,
415 | },
416 | default: 0,
417 | description: 'How many records to return',
418 | },
419 | {
420 | displayName: 'API Version',
421 | name: 'version',
422 | type: 'options',
423 | options: [
424 | {
425 | name: 'v1',
426 | value: 'v1',
427 | },
428 | ],
429 | displayOptions: {
430 | show: {
431 | operation: [
432 | 'getRecord',
433 | 'listRecords',
434 | 'insertRecord',
435 | 'updateRecord',
436 | 'removeRecord',
437 | 'createRecord',
438 | 'runSuiteQL',
439 | ],
440 | },
441 | },
442 | default: 'v1',
443 | },
444 | {
445 | displayName: 'Options',
446 | name: 'options',
447 | type: 'collection',
448 | default: {},
449 | placeholder: 'Add options',
450 | description: 'Add options',
451 | options: [
452 | {
453 | displayName: 'Concurrency',
454 | name: 'concurrency',
455 | type: 'number',
456 | default: 1,
457 | typeOptions: {
458 | minValue: 1,
459 | },
460 | description: 'Use control the maximum number of REST requests sent to NetSuite at the same time. The default is 1.',
461 | },
462 | {
463 | displayName: 'Full Response',
464 | name: 'fullResponse',
465 | type: 'boolean',
466 | default: false,
467 | description: 'Returns the full reponse data instead of only the body',
468 | },
469 | ],
470 | },
471 | ],
472 | };
473 |
--------------------------------------------------------------------------------
/nodes/NetSuite/NetSuite.node.ts:
--------------------------------------------------------------------------------
1 | import { debuglog } from 'util';
2 | import {
3 | IDataObject,
4 | IExecuteFunctions,
5 | INodeExecutionData,
6 | INodeType,
7 | INodeTypeDescription,
8 | JsonObject,
9 | NodeApiError,
10 | } from 'n8n-workflow';
11 |
12 | import {
13 | INetSuiteCredentials,
14 | INetSuiteOperationOptions,
15 | INetSuitePagedBody,
16 | INetSuiteRequestOptions,
17 | INetSuiteResponse,
18 | NetSuiteRequestType,
19 | } from './NetSuite.node.types';
20 |
21 | import {
22 | nodeDescription,
23 | } from './NetSuite.node.options';
24 |
25 | import { makeRequest } from '@fye/netsuite-rest-api';
26 |
27 | import pLimit from 'p-limit';
28 |
29 | const debug = debuglog('n8n-nodes-netsuite');
30 |
31 | const handleNetsuiteResponse = (fns: IExecuteFunctions, response: INetSuiteResponse) => {
32 | // debug(response);
33 | debug(`Netsuite response:`, response.statusCode, response.body);
34 | let body: JsonObject = {};
35 | const {
36 | title: webTitle = undefined,
37 | // code: restletCode = undefined,
38 | 'o:errorCode': webCode,
39 | 'o:errorDetails': webDetails,
40 | message: restletMessage = undefined,
41 | } = response.body;
42 | if (!(response.statusCode && response.statusCode >= 200 && response.statusCode < 400)) {
43 | let message = webTitle || restletMessage || webCode || response.statusText;
44 | if (webDetails && webDetails.length > 0) {
45 | message = webDetails[0].detail || message;
46 | }
47 | if (fns.continueOnFail() !== true) {
48 | // const code = webCode || restletCode;
49 | const error = new NodeApiError(fns.getNode(), response.body);
50 | error.message = message;
51 | throw error;
52 | } else {
53 | body = {
54 | error: message,
55 | };
56 | }
57 | } else {
58 | body = response.body;
59 | if ([ 'POST', 'PATCH', 'DELETE' ].includes(response.request.options.method)) {
60 | body = typeof body === 'object' ? response.body : {};
61 | if (response.headers['x-netsuite-propertyvalidation']) {
62 | body.propertyValidation = response.headers['x-netsuite-propertyvalidation'].split(',');
63 | }
64 | if (response.headers['x-n-operationid']) {
65 | body.operationId = response.headers['x-n-operationid'];
66 | }
67 | if (response.headers['x-netsuite-jobid']) {
68 | body.jobId = response.headers['x-netsuite-jobid'];
69 | }
70 | if (response.headers['location']) {
71 | body.links = [
72 | {
73 | rel: 'self',
74 | href: response.headers['location'],
75 | },
76 | ];
77 | body.id = response.headers['location'].split('/').pop();
78 | }
79 | body.success = response.statusCode === 204;
80 | }
81 | }
82 | // debug(body);
83 | return { json: body };
84 | };
85 |
86 | const getConfig = (credentials: INetSuiteCredentials) => ({
87 | netsuiteApiHost: credentials.hostname,
88 | consumerKey: credentials.consumerKey,
89 | consumerSecret: credentials.consumerSecret,
90 | netsuiteAccountId: credentials.accountId,
91 | netsuiteTokenKey: credentials.tokenKey,
92 | netsuiteTokenSecret: credentials.tokenSecret,
93 | netsuiteQueryLimit: 1000,
94 | });
95 |
96 | export class NetSuite implements INodeType {
97 | description: INodeTypeDescription = nodeDescription;
98 |
99 | static getRecordType({ fns, itemIndex }: INetSuiteOperationOptions): string {
100 | let recordType = fns.getNodeParameter('recordType', itemIndex) as string;
101 | if (recordType === 'custom') {
102 | recordType = fns.getNodeParameter('customRecordTypeScriptId', itemIndex) as string;
103 | }
104 | return recordType;
105 | }
106 |
107 | static async listRecords(options: INetSuiteOperationOptions): Promise {
108 | const { fns, credentials, itemIndex } = options;
109 | const nodeContext = fns.getContext('node');
110 | const apiVersion = fns.getNodeParameter('version', itemIndex) as string;
111 | const recordType = NetSuite.getRecordType(options);
112 | const returnAll = fns.getNodeParameter('returnAll', itemIndex) as boolean;
113 | const query = fns.getNodeParameter('query', itemIndex) as string;
114 | let limit = 100;
115 | let offset = 0;
116 | let hasMore = true;
117 | const method = 'GET';
118 | let nextUrl;
119 | const requestType = NetSuiteRequestType.Record;
120 | const params = new URLSearchParams();
121 | const returnData: INodeExecutionData[] = [];
122 | let prefix = query ? `?${query}` : '';
123 | if (returnAll !== true) {
124 | prefix = query ? `${prefix}&` : '?';
125 | limit = fns.getNodeParameter('limit', itemIndex) as number || limit;
126 | offset = fns.getNodeParameter('offset', itemIndex) as number || offset;
127 | params.set('limit', String(limit));
128 | params.set('offset', String(offset));
129 | prefix += params.toString();
130 | }
131 | const requestData: INetSuiteRequestOptions = {
132 | method,
133 | requestType,
134 | path: `services/rest/record/${apiVersion}/${recordType}${prefix}`,
135 | };
136 | nodeContext.hasMore = hasMore;
137 | nodeContext.count = limit;
138 | nodeContext.offset = offset;
139 | // debug('requestData', requestData);
140 | while ((returnAll || returnData.length < limit) && hasMore === true) {
141 | const response = await makeRequest(getConfig(credentials), requestData);
142 | const body: JsonObject = handleNetsuiteResponse(fns, response);
143 | const { hasMore: doContinue, items, links, offset, count, totalResults } = (body.json as INetSuitePagedBody);
144 | if (doContinue) {
145 | nextUrl = (links.find((link) => link.rel === 'next') || {}).href;
146 | requestData.nextUrl = nextUrl;
147 | }
148 | if (Array.isArray(items)) {
149 | for (const json of items) {
150 | if (returnAll || returnData.length < limit) {
151 | returnData.push({ json });
152 | }
153 | }
154 | }
155 | hasMore = doContinue && (returnAll || returnData.length < limit);
156 | nodeContext.hasMore = doContinue;
157 | nodeContext.count = count;
158 | nodeContext.offset = offset;
159 | nodeContext.totalResults = totalResults;
160 | if (requestData.nextUrl) {
161 | nodeContext.nextUrl = requestData.nextUrl;
162 | }
163 | }
164 | return returnData;
165 | }
166 |
167 | static async runSuiteQL(options: INetSuiteOperationOptions): Promise {
168 | const { fns, credentials, itemIndex } = options;
169 | const nodeContext = fns.getContext('node');
170 | const apiVersion = fns.getNodeParameter('version', itemIndex) as string;
171 | const returnAll = fns.getNodeParameter('returnAll', itemIndex) as boolean;
172 | const query = fns.getNodeParameter('query', itemIndex) as string;
173 | let limit = 1000;
174 | let offset = 0;
175 | let hasMore = true;
176 | const method = 'POST';
177 | let nextUrl;
178 | const requestType = NetSuiteRequestType.SuiteQL;
179 | const params = new URLSearchParams();
180 | const returnData: INodeExecutionData[] = [];
181 | const config = getConfig(credentials);
182 | let prefix = '?';
183 | if (returnAll !== true) {
184 | limit = fns.getNodeParameter('limit', itemIndex) as number || limit;
185 | offset = fns.getNodeParameter('offset', itemIndex) as number || offset;
186 | params.set('offset', String(offset));
187 | }
188 | params.set('limit', String(limit));
189 | config.netsuiteQueryLimit = limit;
190 | prefix += params.toString();
191 | const requestData: INetSuiteRequestOptions = {
192 | method,
193 | requestType,
194 | query,
195 | path: `services/rest/query/${apiVersion}/suiteql${prefix}`,
196 | };
197 | nodeContext.hasMore = hasMore;
198 | nodeContext.count = limit;
199 | nodeContext.offset = offset;
200 | debug('requestData', requestData);
201 | while ((returnAll || returnData.length < limit) && hasMore === true) {
202 | const response = await makeRequest(config, requestData);
203 | const body: JsonObject = handleNetsuiteResponse(fns, response);
204 | const { hasMore: doContinue, items, links, count, totalResults, offset } = (body.json as INetSuitePagedBody);
205 | if (doContinue) {
206 | nextUrl = (links.find((link) => link.rel === 'next') || {}).href;
207 | requestData.nextUrl = nextUrl;
208 | }
209 | if (Array.isArray(items)) {
210 | for (const json of items) {
211 | if (returnAll || returnData.length < limit) {
212 | returnData.push({ json });
213 | }
214 | }
215 | }
216 | hasMore = doContinue && (returnAll || returnData.length < limit);
217 | nodeContext.hasMore = doContinue;
218 | nodeContext.count = count;
219 | nodeContext.offset = offset;
220 | nodeContext.totalResults = totalResults;
221 | if (requestData.nextUrl) {
222 | nodeContext.nextUrl = requestData.nextUrl;
223 | }
224 | }
225 | return returnData;
226 | }
227 |
228 | static async getRecord(options: INetSuiteOperationOptions): Promise {
229 | const { item, fns, credentials, itemIndex } = options;
230 | const params = new URLSearchParams();
231 | const expandSubResources = fns.getNodeParameter('expandSubResources', itemIndex) as boolean;
232 | const simpleEnumFormat = fns.getNodeParameter('simpleEnumFormat', itemIndex) as boolean;
233 | const apiVersion = fns.getNodeParameter('version', itemIndex) as string;
234 | const recordType = NetSuite.getRecordType(options);
235 | const internalId = fns.getNodeParameter('internalId', itemIndex) as string;
236 | if (expandSubResources) {
237 | params.append('expandSubResources', 'true');
238 | }
239 | if (simpleEnumFormat) {
240 | params.append('simpleEnumFormat', 'true');
241 | }
242 | const q = params.toString();
243 | const requestData = {
244 | method: 'GET',
245 | requestType: NetSuiteRequestType.Record,
246 | path: `services/rest/record/${apiVersion}/${recordType}/${internalId}${q ? `?${q}` : ''}`,
247 | };
248 | const response = await makeRequest(getConfig(credentials), requestData);
249 | if (item) response.body.orderNo = item.json.orderNo;
250 | return handleNetsuiteResponse(fns, response);
251 | }
252 |
253 | static async removeRecord(options: INetSuiteOperationOptions): Promise {
254 | const { fns, credentials, itemIndex } = options;
255 | const apiVersion = fns.getNodeParameter('version', itemIndex) as string;
256 | const recordType = NetSuite.getRecordType(options);
257 | const internalId = fns.getNodeParameter('internalId', itemIndex) as string;
258 | const requestData = {
259 | method: 'DELETE',
260 | requestType: NetSuiteRequestType.Record,
261 | path: `services/rest/record/${apiVersion}/${recordType}/${internalId}`,
262 | };
263 | const response = await makeRequest(getConfig(credentials), requestData);
264 | return handleNetsuiteResponse(fns, response);
265 | }
266 |
267 | static async insertRecord(options: INetSuiteOperationOptions): Promise {
268 | const { fns, credentials, itemIndex, item } = options;
269 | const apiVersion = fns.getNodeParameter('version', itemIndex) as string;
270 | const recordType = NetSuite.getRecordType(options);
271 | const query = item ? item.json : undefined;
272 | const requestData: INetSuiteRequestOptions = {
273 | method: 'POST',
274 | requestType: NetSuiteRequestType.Record,
275 | path: `services/rest/record/${apiVersion}/${recordType}`,
276 | };
277 | if (query) requestData.query = query;
278 | const response = await makeRequest(getConfig(credentials), requestData);
279 | return handleNetsuiteResponse(fns, response);
280 | }
281 |
282 | static async updateRecord(options: INetSuiteOperationOptions): Promise {
283 | const { fns, credentials, itemIndex, item } = options;
284 | const apiVersion = fns.getNodeParameter('version', itemIndex) as string;
285 | const recordType = NetSuite.getRecordType(options);
286 | const internalId = fns.getNodeParameter('internalId', itemIndex) as string;
287 | const query = item ? item.json : undefined;
288 | const requestData: INetSuiteRequestOptions = {
289 | method: 'PATCH',
290 | requestType: NetSuiteRequestType.Record,
291 | path: `services/rest/record/${apiVersion}/${recordType}/${internalId}`,
292 | };
293 | if (query) requestData.query = query;
294 | const response = await makeRequest(getConfig(credentials), requestData);
295 | return handleNetsuiteResponse(fns, response);
296 | }
297 |
298 | static async rawRequest(options: INetSuiteOperationOptions): Promise {
299 | const { fns, credentials, itemIndex, item } = options;
300 | const nodeContext = fns.getContext('node');
301 | let path = fns.getNodeParameter('path', itemIndex) as string;
302 | const method = fns.getNodeParameter('method', itemIndex) as string;
303 | const body = fns.getNodeParameter('body', itemIndex) as string;
304 | const requestType = fns.getNodeParameter('requestType', itemIndex) as NetSuiteRequestType;
305 | const query = body || (item ? item.json : undefined);
306 | const nodeOptions = fns.getNodeParameter('options', 0) as IDataObject;
307 |
308 | if (path && (path.startsWith('https://') || path.startsWith('http://'))) {
309 | const url = new URL(path);
310 | path = `${url.pathname.replace(/^\//, '')}${url.search || ''}`;
311 | }
312 |
313 | const requestData: INetSuiteRequestOptions = {
314 | method,
315 | requestType,
316 | path,
317 | };
318 | if (query && !['GET', 'HEAD', 'OPTIONS'].includes(method)) requestData.query = query;
319 | // debug('requestData', requestData);
320 | const response = await makeRequest(getConfig(credentials), requestData);
321 |
322 | if (response.body) {
323 | nodeContext.hasMore = response.body.hasMore;
324 | nodeContext.count = response.body.count;
325 | nodeContext.offset = response.body.offset;
326 | nodeContext.totalResults = response.body.totalResults;
327 | }
328 |
329 | if (nodeOptions.fullResponse) {
330 | return {
331 | json: {
332 | statusCode: response.statusCode,
333 | headers: response.headers,
334 | body: response.body,
335 | },
336 | };
337 | } else {
338 | return { json: response.body };
339 | }
340 | }
341 |
342 | async execute(this: IExecuteFunctions): Promise {
343 | const credentials: INetSuiteCredentials = (await this.getCredentials('netsuite')) as INetSuiteCredentials;
344 | const operation = this.getNodeParameter('operation', 0) as string;
345 | const items: INodeExecutionData[] = this.getInputData();
346 | const returnData: INodeExecutionData[] = [];
347 | const promises = [];
348 | const options = this.getNodeParameter('options', 0) as IDataObject;
349 | const concurrency = options.concurrency as number || 1;
350 | const limit = pLimit(concurrency);
351 |
352 | for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
353 | const item: INodeExecutionData = items[itemIndex];
354 | let data: INodeExecutionData | INodeExecutionData[];
355 |
356 | promises.push(limit(async () =>{
357 | debug(`Processing ${operation} for ${itemIndex+1} of ${items.length}`);
358 | if (operation === 'getRecord') {
359 | data = await NetSuite.getRecord({ item, fns: this, credentials, itemIndex});
360 | } else if (operation === 'listRecords') {
361 | data = await NetSuite.listRecords({ item, fns: this, credentials, itemIndex});
362 | } else if (operation === 'removeRecord') {
363 | data = await NetSuite.removeRecord({ item, fns: this, credentials, itemIndex});
364 | } else if (operation === 'insertRecord') {
365 | data = await NetSuite.insertRecord({ item, fns: this, credentials, itemIndex});
366 | } else if (operation === 'updateRecord') {
367 | data = await NetSuite.updateRecord({ item, fns: this, credentials, itemIndex});
368 | } else if (operation === 'rawRequest') {
369 | data = await NetSuite.rawRequest({ item, fns: this, credentials, itemIndex});
370 | } else if (operation === 'runSuiteQL') {
371 | data = await NetSuite.runSuiteQL({ item, fns: this, credentials, itemIndex});
372 | } else {
373 | const error = `The operation "${operation}" is not supported!`;
374 | if (this.continueOnFail() !== true) {
375 | throw new Error(error);
376 | } else {
377 | data = { json: { error } };
378 | }
379 | }
380 | return data;
381 | }));
382 | }
383 |
384 | const results = await Promise.all(promises);
385 | for await (const result of results) {
386 | if (result) {
387 | if (Array.isArray(result)) {
388 | returnData.push(...result);
389 | } else {
390 | returnData.push(result);
391 | }
392 | }
393 | }
394 |
395 | return this.prepareOutputData(returnData);
396 | }
397 | }
398 |
--------------------------------------------------------------------------------