n8n-nodes-james-pro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # n8n-nodes-james-pro
2
+
3
+ Community node for the James PRO API.
4
+
5
+ ## Credentials
6
+
7
+ The James PRO API uses HTTP Basic Auth with an API `key` as username and `secret` as password.
8
+ Request these credentials from `https://start.jamespro.nl/api/auth/` using your James PRO username, password, and a device type.
9
+
10
+ ## Supported resources
11
+
12
+ - Branch
13
+ - Company
14
+ - Contact
15
+ - File
16
+ - Invoice
17
+ - Me
18
+ - Note
19
+ - Offer
20
+ - Project
21
+ - Setting
22
+ - Tag
23
+ - Task
24
+
25
+ List operations support `limit`, `page`, `fields`, `include`, and JSON filters such as:
26
+
27
+ ```json
28
+ {
29
+ "name": "James PRO B.V.",
30
+ "client": 1
31
+ }
32
+ ```
33
+
34
+ Create and update operations accept a JSON request body so the node can cover all fields documented by James PRO without forcing a narrow form UI.
35
+
36
+ ## Development
37
+
38
+ ```bash
39
+ npm install
40
+ npm run build
41
+ ```
42
+
43
+ To test locally in n8n, install this package as a community node or link it into your n8n custom extensions setup.
@@ -0,0 +1,9 @@
1
+ import type { ICredentialTestRequest, ICredentialType, INodeProperties, IAuthenticateGeneric } from 'n8n-workflow';
2
+ export declare class JamesProApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JamesProApi = void 0;
4
+ class JamesProApi {
5
+ name = 'jamesProApi';
6
+ displayName = 'James PRO API';
7
+ documentationUrl = 'https://start.jamespro.nl/api';
8
+ properties = [
9
+ {
10
+ displayName: 'Base URL',
11
+ name: 'baseUrl',
12
+ type: 'string',
13
+ default: 'https://start.jamespro.nl/api',
14
+ required: true,
15
+ description: 'Base URL of the James PRO API',
16
+ },
17
+ {
18
+ displayName: 'API Key',
19
+ name: 'key',
20
+ type: 'string',
21
+ default: '',
22
+ required: true,
23
+ description: 'James PRO API key. This is used as the Basic Auth username.',
24
+ },
25
+ {
26
+ displayName: 'API Secret',
27
+ name: 'secret',
28
+ type: 'string',
29
+ typeOptions: {
30
+ password: true,
31
+ },
32
+ default: '',
33
+ required: true,
34
+ description: 'James PRO API secret. This is used as the Basic Auth password.',
35
+ },
36
+ ];
37
+ authenticate = {
38
+ type: 'generic',
39
+ properties: {
40
+ auth: {
41
+ username: '={{$credentials.key}}',
42
+ password: '={{$credentials.secret}}',
43
+ },
44
+ },
45
+ };
46
+ test = {
47
+ request: {
48
+ baseURL: '={{$credentials.baseUrl}}',
49
+ url: '/me/',
50
+ method: 'GET',
51
+ },
52
+ };
53
+ }
54
+ exports.JamesProApi = JamesProApi;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class JamesPro implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,675 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JamesPro = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const RESOURCE_CONFIGS = [
6
+ {
7
+ displayName: 'Branch',
8
+ name: 'branch',
9
+ listPath: '/branches/',
10
+ itemPath: '/branch',
11
+ canCreate: true,
12
+ canUpdate: true,
13
+ canDelete: true,
14
+ },
15
+ {
16
+ displayName: 'Company',
17
+ name: 'company',
18
+ listPath: '/companies/',
19
+ itemPath: '/company',
20
+ canCreate: true,
21
+ canUpdate: true,
22
+ canDelete: true,
23
+ },
24
+ {
25
+ displayName: 'Contact',
26
+ name: 'contact',
27
+ listPath: '/contacts/',
28
+ itemPath: '/contact',
29
+ canCreate: true,
30
+ canUpdate: true,
31
+ canDelete: true,
32
+ },
33
+ {
34
+ displayName: 'File',
35
+ name: 'file',
36
+ listPath: '/files/',
37
+ itemPath: '/file',
38
+ canCreate: true,
39
+ canUpdate: true,
40
+ canDelete: true,
41
+ },
42
+ {
43
+ displayName: 'Invoice',
44
+ name: 'invoice',
45
+ listPath: '/invoices/',
46
+ itemPath: '/invoice',
47
+ canCreate: true,
48
+ canUpdate: true,
49
+ canDelete: true,
50
+ },
51
+ {
52
+ displayName: 'Me',
53
+ name: 'me',
54
+ itemPath: '/me/',
55
+ canCreate: false,
56
+ canUpdate: true,
57
+ canDelete: false,
58
+ },
59
+ {
60
+ displayName: 'Note',
61
+ name: 'note',
62
+ listPath: '/notes/',
63
+ itemPath: '/note',
64
+ canCreate: true,
65
+ canUpdate: true,
66
+ canDelete: true,
67
+ },
68
+ {
69
+ displayName: 'Offer',
70
+ name: 'offer',
71
+ listPath: '/offers/',
72
+ itemPath: '/offer',
73
+ canCreate: true,
74
+ canUpdate: true,
75
+ canDelete: true,
76
+ },
77
+ {
78
+ displayName: 'Project',
79
+ name: 'project',
80
+ listPath: '/projects/',
81
+ itemPath: '/project',
82
+ canCreate: true,
83
+ canUpdate: true,
84
+ canDelete: true,
85
+ },
86
+ {
87
+ displayName: 'Setting',
88
+ name: 'setting',
89
+ listPath: '/settings/',
90
+ itemPath: '/setting',
91
+ canCreate: true,
92
+ canUpdate: true,
93
+ canDelete: true,
94
+ },
95
+ {
96
+ displayName: 'Tag',
97
+ name: 'tag',
98
+ listPath: '/tags/',
99
+ itemPath: '/tag',
100
+ canCreate: true,
101
+ canUpdate: true,
102
+ canDelete: true,
103
+ },
104
+ {
105
+ displayName: 'Task',
106
+ name: 'task',
107
+ listPath: '/tasks/',
108
+ itemPath: '/task',
109
+ canCreate: true,
110
+ canUpdate: true,
111
+ canDelete: true,
112
+ },
113
+ ];
114
+ const crudResources = RESOURCE_CONFIGS.filter((resource) => resource.name !== 'me').map((resource) => resource.name);
115
+ const listResources = RESOURCE_CONFIGS.filter((resource) => resource.listPath).map((resource) => resource.name);
116
+ const getResources = RESOURCE_CONFIGS.map((resource) => resource.name);
117
+ const commonQueryProperties = [
118
+ {
119
+ displayName: 'Fields',
120
+ name: 'fields',
121
+ type: 'string',
122
+ default: '',
123
+ description: 'Comma-separated list of fields to return, for example: id,name,email',
124
+ displayOptions: {
125
+ show: {
126
+ resource: getResources,
127
+ operation: ['get', 'getAll'],
128
+ },
129
+ },
130
+ },
131
+ {
132
+ displayName: 'Include',
133
+ name: 'include',
134
+ type: 'string',
135
+ default: '',
136
+ description: 'Related information to include when supported by the endpoint',
137
+ displayOptions: {
138
+ show: {
139
+ resource: getResources,
140
+ operation: ['get', 'getAll'],
141
+ },
142
+ },
143
+ },
144
+ ];
145
+ const listQueryProperties = [
146
+ {
147
+ displayName: 'Return All',
148
+ name: 'returnAll',
149
+ type: 'boolean',
150
+ default: false,
151
+ description: 'Whether to return all results by paging through the API response',
152
+ displayOptions: {
153
+ show: {
154
+ resource: listResources,
155
+ operation: ['getAll'],
156
+ },
157
+ },
158
+ },
159
+ {
160
+ displayName: 'Limit',
161
+ name: 'limit',
162
+ type: 'string',
163
+ default: '30',
164
+ description: 'Maximum number of records to return. James PRO also supports offset syntax such as 200,100.',
165
+ displayOptions: {
166
+ show: {
167
+ resource: listResources,
168
+ operation: ['getAll'],
169
+ },
170
+ },
171
+ },
172
+ {
173
+ displayName: 'Page',
174
+ name: 'page',
175
+ type: 'number',
176
+ typeOptions: {
177
+ minValue: 1,
178
+ },
179
+ default: 1,
180
+ description: 'Page number to request when Return All is disabled',
181
+ displayOptions: {
182
+ show: {
183
+ resource: listResources,
184
+ operation: ['getAll'],
185
+ returnAll: [false],
186
+ },
187
+ },
188
+ },
189
+ {
190
+ displayName: 'Filter JSON',
191
+ name: 'filterJson',
192
+ type: 'json',
193
+ default: '{}',
194
+ description: 'JSON object converted to filter[column]=value query parameters, for example {"name":"James PRO B.V."}',
195
+ displayOptions: {
196
+ show: {
197
+ resource: listResources,
198
+ operation: ['getAll'],
199
+ },
200
+ },
201
+ },
202
+ ];
203
+ class JamesPro {
204
+ description = {
205
+ displayName: 'James PRO',
206
+ name: 'jamesPro',
207
+ icon: 'file:jamespro.svg',
208
+ group: ['transform'],
209
+ version: 1,
210
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
211
+ description: 'Read and write data in James PRO',
212
+ defaults: {
213
+ name: 'James PRO',
214
+ },
215
+ usableAsTool: true,
216
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
217
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
218
+ credentials: [
219
+ {
220
+ name: 'jamesProApi',
221
+ required: true,
222
+ },
223
+ ],
224
+ properties: [
225
+ {
226
+ displayName: 'Resource',
227
+ name: 'resource',
228
+ type: 'options',
229
+ noDataExpression: true,
230
+ options: [
231
+ ...RESOURCE_CONFIGS.map((resource) => ({
232
+ name: resource.displayName,
233
+ value: resource.name,
234
+ })),
235
+ {
236
+ name: 'Custom Request',
237
+ value: 'customRequest',
238
+ },
239
+ ],
240
+ default: 'company',
241
+ },
242
+ {
243
+ displayName: 'Operation',
244
+ name: 'operation',
245
+ type: 'options',
246
+ noDataExpression: true,
247
+ displayOptions: {
248
+ show: {
249
+ resource: crudResources,
250
+ },
251
+ },
252
+ options: [
253
+ {
254
+ name: 'Get Many',
255
+ value: 'getAll',
256
+ description: 'Get many records',
257
+ action: 'Get many records',
258
+ },
259
+ {
260
+ name: 'Get',
261
+ value: 'get',
262
+ description: 'Get one record',
263
+ action: 'Get one record',
264
+ },
265
+ {
266
+ name: 'Create',
267
+ value: 'create',
268
+ description: 'Create a record',
269
+ action: 'Create a record',
270
+ },
271
+ {
272
+ name: 'Update',
273
+ value: 'update',
274
+ description: 'Update a record',
275
+ action: 'Update a record',
276
+ },
277
+ {
278
+ name: 'Delete',
279
+ value: 'delete',
280
+ description: 'Delete a record',
281
+ action: 'Delete a record',
282
+ },
283
+ ],
284
+ default: 'getAll',
285
+ },
286
+ {
287
+ displayName: 'Operation',
288
+ name: 'operation',
289
+ type: 'options',
290
+ noDataExpression: true,
291
+ displayOptions: {
292
+ show: {
293
+ resource: ['me'],
294
+ },
295
+ },
296
+ options: [
297
+ {
298
+ name: 'Get',
299
+ value: 'get',
300
+ description: 'Get authenticated user information',
301
+ action: 'Get authenticated user information',
302
+ },
303
+ {
304
+ name: 'Update',
305
+ value: 'update',
306
+ description: 'Update authenticated user information',
307
+ action: 'Update authenticated user information',
308
+ },
309
+ ],
310
+ default: 'get',
311
+ },
312
+ {
313
+ displayName: 'Operation',
314
+ name: 'operation',
315
+ type: 'options',
316
+ noDataExpression: true,
317
+ displayOptions: {
318
+ show: {
319
+ resource: ['customRequest'],
320
+ },
321
+ },
322
+ options: [
323
+ {
324
+ name: 'Send',
325
+ value: 'send',
326
+ description: 'Send a custom request to the James PRO API',
327
+ action: 'Send a custom request',
328
+ },
329
+ ],
330
+ default: 'send',
331
+ },
332
+ {
333
+ displayName: 'ID or Name',
334
+ name: 'entityId',
335
+ type: 'string',
336
+ required: true,
337
+ default: '',
338
+ description: 'The entity ID. For Setting, use the setting name, for example settingTendertextcolor.',
339
+ displayOptions: {
340
+ show: {
341
+ resource: crudResources,
342
+ operation: ['get', 'update', 'delete'],
343
+ },
344
+ },
345
+ },
346
+ ...commonQueryProperties,
347
+ ...listQueryProperties,
348
+ {
349
+ displayName: 'Body JSON',
350
+ name: 'bodyJson',
351
+ type: 'json',
352
+ default: '{}',
353
+ required: true,
354
+ description: 'Request body as JSON',
355
+ displayOptions: {
356
+ show: {
357
+ resource: [...crudResources, 'me'],
358
+ operation: ['create', 'update'],
359
+ },
360
+ },
361
+ },
362
+ {
363
+ displayName: 'Confirm Delete',
364
+ name: 'confirmDelete',
365
+ type: 'boolean',
366
+ default: true,
367
+ description: 'Whether to send {"confirm":true} in the delete request body',
368
+ displayOptions: {
369
+ show: {
370
+ resource: crudResources,
371
+ operation: ['delete'],
372
+ },
373
+ },
374
+ },
375
+ {
376
+ displayName: 'Method',
377
+ name: 'method',
378
+ type: 'options',
379
+ options: [
380
+ { name: 'GET', value: 'GET' },
381
+ { name: 'POST', value: 'POST' },
382
+ { name: 'PUT', value: 'PUT' },
383
+ { name: 'DELETE', value: 'DELETE' },
384
+ ],
385
+ default: 'GET',
386
+ displayOptions: {
387
+ show: {
388
+ resource: ['customRequest'],
389
+ operation: ['send'],
390
+ },
391
+ },
392
+ },
393
+ {
394
+ displayName: 'Endpoint',
395
+ name: 'endpoint',
396
+ type: 'string',
397
+ default: '/companies/',
398
+ required: true,
399
+ description: 'API endpoint path, for example /companies/ or /company/1000',
400
+ displayOptions: {
401
+ show: {
402
+ resource: ['customRequest'],
403
+ operation: ['send'],
404
+ },
405
+ },
406
+ },
407
+ {
408
+ displayName: 'Query JSON',
409
+ name: 'queryJson',
410
+ type: 'json',
411
+ default: '{}',
412
+ description: 'Query string parameters as JSON',
413
+ displayOptions: {
414
+ show: {
415
+ resource: ['customRequest'],
416
+ operation: ['send'],
417
+ },
418
+ },
419
+ },
420
+ {
421
+ displayName: 'Body JSON',
422
+ name: 'customBodyJson',
423
+ type: 'json',
424
+ default: '{}',
425
+ description: 'Request body as JSON',
426
+ displayOptions: {
427
+ show: {
428
+ resource: ['customRequest'],
429
+ operation: ['send'],
430
+ method: ['POST', 'PUT', 'DELETE'],
431
+ },
432
+ },
433
+ },
434
+ {
435
+ displayName: 'Return Body Only',
436
+ name: 'returnBodyOnly',
437
+ type: 'boolean',
438
+ default: true,
439
+ description: 'Whether to return the James PRO response body instead of the full response envelope',
440
+ },
441
+ ],
442
+ };
443
+ async execute() {
444
+ const items = this.getInputData();
445
+ const returnData = [];
446
+ const credentials = await this.getCredentials('jamesProApi');
447
+ const baseURL = String(credentials.baseUrl).replace(/\/+$/, '');
448
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
449
+ try {
450
+ const resource = this.getNodeParameter('resource', itemIndex);
451
+ const operation = this.getNodeParameter('operation', itemIndex);
452
+ const returnBodyOnly = this.getNodeParameter('returnBodyOnly', itemIndex, true);
453
+ if (resource === 'customRequest') {
454
+ const response = await sendJamesProRequest.call(this, itemIndex, {
455
+ baseURL,
456
+ method: this.getNodeParameter('method', itemIndex),
457
+ url: normalizeEndpoint(this.getNodeParameter('endpoint', itemIndex)),
458
+ qs: parseJsonParameter.call(this, 'queryJson', itemIndex),
459
+ body: this.getNodeParameter('method', itemIndex) === 'GET'
460
+ ? undefined
461
+ : parseJsonParameter.call(this, 'customBodyJson', itemIndex),
462
+ });
463
+ returnData.push(...normalizeResponse.call(this, response, returnBodyOnly));
464
+ continue;
465
+ }
466
+ const resourceConfig = RESOURCE_CONFIGS.find((config) => config.name === resource);
467
+ if (!resourceConfig) {
468
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported resource: ${resource}`, {
469
+ itemIndex,
470
+ });
471
+ }
472
+ if (operation === 'getAll') {
473
+ if (!resourceConfig.listPath) {
474
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${resourceConfig.displayName} does not support Get Many`, { itemIndex });
475
+ }
476
+ const returnAll = this.getNodeParameter('returnAll', itemIndex, false);
477
+ const responses = await getMany.call(this, resourceConfig.listPath, itemIndex, baseURL, returnAll);
478
+ for (const response of responses) {
479
+ returnData.push(...normalizeResponse.call(this, response, returnBodyOnly));
480
+ }
481
+ continue;
482
+ }
483
+ if (operation === 'get') {
484
+ const url = resource === 'me'
485
+ ? resourceConfig.itemPath
486
+ : `${resourceConfig.itemPath}/${encodeURIComponent(this.getNodeParameter('entityId', itemIndex))}`;
487
+ const response = await sendJamesProRequest.call(this, itemIndex, {
488
+ baseURL,
489
+ method: 'GET',
490
+ url,
491
+ qs: buildQuery.call(this, itemIndex),
492
+ });
493
+ returnData.push(...normalizeResponse.call(this, response, returnBodyOnly));
494
+ continue;
495
+ }
496
+ if (operation === 'create') {
497
+ if (!resourceConfig.canCreate) {
498
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${resourceConfig.displayName} does not support Create`, { itemIndex });
499
+ }
500
+ const response = await sendJamesProRequest.call(this, itemIndex, {
501
+ baseURL,
502
+ method: 'POST',
503
+ url: resourceConfig.itemPath,
504
+ body: parseJsonParameter.call(this, 'bodyJson', itemIndex),
505
+ });
506
+ returnData.push(...normalizeResponse.call(this, response, returnBodyOnly));
507
+ continue;
508
+ }
509
+ if (operation === 'update') {
510
+ if (!resourceConfig.canUpdate) {
511
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${resourceConfig.displayName} does not support Update`, { itemIndex });
512
+ }
513
+ const url = resource === 'me'
514
+ ? resourceConfig.itemPath
515
+ : `${resourceConfig.itemPath}/${encodeURIComponent(this.getNodeParameter('entityId', itemIndex))}`;
516
+ const response = await sendJamesProRequest.call(this, itemIndex, {
517
+ baseURL,
518
+ method: 'PUT',
519
+ url,
520
+ body: parseJsonParameter.call(this, 'bodyJson', itemIndex),
521
+ });
522
+ returnData.push(...normalizeResponse.call(this, response, returnBodyOnly));
523
+ continue;
524
+ }
525
+ if (operation === 'delete') {
526
+ if (!resourceConfig.canDelete) {
527
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${resourceConfig.displayName} does not support Delete`, { itemIndex });
528
+ }
529
+ const response = await sendJamesProRequest.call(this, itemIndex, {
530
+ baseURL,
531
+ method: 'DELETE',
532
+ url: `${resourceConfig.itemPath}/${encodeURIComponent(this.getNodeParameter('entityId', itemIndex))}`,
533
+ body: {
534
+ confirm: this.getNodeParameter('confirmDelete', itemIndex, true),
535
+ },
536
+ });
537
+ returnData.push(...normalizeResponse.call(this, response, returnBodyOnly));
538
+ continue;
539
+ }
540
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported operation: ${operation}`, {
541
+ itemIndex,
542
+ });
543
+ }
544
+ catch (error) {
545
+ if (this.continueOnFail()) {
546
+ returnData.push({
547
+ json: {
548
+ error: error instanceof Error ? error.message : String(error),
549
+ },
550
+ pairedItem: {
551
+ item: itemIndex,
552
+ },
553
+ });
554
+ continue;
555
+ }
556
+ throw error;
557
+ }
558
+ }
559
+ return [returnData];
560
+ }
561
+ }
562
+ exports.JamesPro = JamesPro;
563
+ async function getMany(path, itemIndex, baseURL, returnAll) {
564
+ const responses = [];
565
+ let page = returnAll ? 1 : this.getNodeParameter('page', itemIndex, 1);
566
+ do {
567
+ const qs = buildQuery.call(this, itemIndex);
568
+ qs.page = page;
569
+ const limit = this.getNodeParameter('limit', itemIndex, '30');
570
+ if (limit) {
571
+ qs.limit = limit;
572
+ }
573
+ const response = await sendJamesProRequest.call(this, itemIndex, {
574
+ baseURL,
575
+ method: 'GET',
576
+ url: path,
577
+ qs,
578
+ });
579
+ responses.push(response);
580
+ if (!returnAll || !response.next) {
581
+ break;
582
+ }
583
+ page += 1;
584
+ } while (true);
585
+ return responses;
586
+ }
587
+ async function sendJamesProRequest(itemIndex, options) {
588
+ const requestOptions = {
589
+ baseURL: options.baseURL,
590
+ url: options.url,
591
+ method: options.method,
592
+ json: true,
593
+ headers: {
594
+ 'Content-Type': 'application/json',
595
+ Accept: 'application/json',
596
+ },
597
+ qs: options.qs,
598
+ };
599
+ if (options.body && Object.keys(options.body).length > 0) {
600
+ requestOptions.body = options.body;
601
+ }
602
+ try {
603
+ return (await this.helpers.httpRequestWithAuthentication.call(this, 'jamesProApi', requestOptions));
604
+ }
605
+ catch (error) {
606
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { itemIndex });
607
+ }
608
+ }
609
+ function buildQuery(itemIndex) {
610
+ const qs = {};
611
+ const fields = this.getNodeParameter('fields', itemIndex, '');
612
+ const include = this.getNodeParameter('include', itemIndex, '');
613
+ if (fields) {
614
+ qs.fields = fields;
615
+ }
616
+ if (include) {
617
+ qs.include = include;
618
+ }
619
+ const filter = parseJsonParameter.call(this, 'filterJson', itemIndex, true);
620
+ for (const [key, value] of Object.entries(filter)) {
621
+ if (value !== undefined) {
622
+ qs[`filter[${key}]`] = value;
623
+ }
624
+ }
625
+ return qs;
626
+ }
627
+ function normalizeEndpoint(endpoint) {
628
+ return endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
629
+ }
630
+ function normalizeResponse(response, returnBodyOnly) {
631
+ const data = returnBodyOnly && 'body' in response ? response.body : response;
632
+ if (Array.isArray(data)) {
633
+ return data.map((item) => ({ json: normalizeItem(item) }));
634
+ }
635
+ return [{ json: normalizeItem(data) }];
636
+ }
637
+ function normalizeItem(value) {
638
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
639
+ return value;
640
+ }
641
+ return {
642
+ value: value === undefined ? null : value,
643
+ };
644
+ }
645
+ function parseJsonParameter(name, itemIndex, allowMissing = false) {
646
+ let value;
647
+ try {
648
+ value = this.getNodeParameter(name, itemIndex, '{}');
649
+ }
650
+ catch (error) {
651
+ if (allowMissing) {
652
+ return {};
653
+ }
654
+ throw error;
655
+ }
656
+ if (typeof value === 'string') {
657
+ if (!value.trim()) {
658
+ return {};
659
+ }
660
+ try {
661
+ value = JSON.parse(value);
662
+ }
663
+ catch {
664
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${name} must contain valid JSON`, {
665
+ itemIndex,
666
+ });
667
+ }
668
+ }
669
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
670
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${name} must be a JSON object`, {
671
+ itemIndex,
672
+ });
673
+ }
674
+ return value;
675
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "node": "n8n-nodes-james-pro.jamesPro",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["Sales"],
6
+ "resources": {
7
+ "credentialDocumentation": [
8
+ {
9
+ "url": "https://start.jamespro.nl/api"
10
+ }
11
+ ]
12
+ }
13
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="James PRO">
2
+ <rect width="64" height="64" rx="12" fill="#1f6feb"/>
3
+ <path fill="#fff" d="M18 15h27v10H29v7h12v9H29c0 5.3-2.1 8-6.3 8-2.1 0-3.9-.5-5.7-1.6v-9.2c1.4 1 2.7 1.5 4 1.5 1.9 0 2.9-1.2 2.9-3.7V25H18V15z"/>
4
+ </svg>
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "n8n-nodes-james-pro",
3
+ "version": "0.1.0",
4
+ "description": "n8n community node for the James PRO API.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/jornboerema/n8n-nodes-james-pro",
7
+ "author": {
8
+ "name": "Jorn Boerema"
9
+ },
10
+ "keywords": [
11
+ "n8n-community-node-package",
12
+ "n8n",
13
+ "james-pro",
14
+ "jamespro"
15
+ ],
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "scripts": {
19
+ "build": "npm run clean && tsc && npm run copy:assets",
20
+ "clean": "rimraf dist",
21
+ "copy:assets": "copyfiles \"nodes/**/*.svg\" \"nodes/**/*.json\" dist",
22
+ "lint": "eslint \"{credentials,nodes}/**/*.ts\"",
23
+ "format": "prettier --write \"{credentials,nodes}/**/*.ts\" \"*.json\" \"*.md\"",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "n8n": {
30
+ "n8nNodesApiVersion": 1,
31
+ "credentials": [
32
+ "dist/credentials/JamesProApi.credentials.js"
33
+ ],
34
+ "nodes": [
35
+ "dist/nodes/JamesPro/JamesPro.node.js"
36
+ ]
37
+ },
38
+ "dependencies": {
39
+ "n8n-workflow": "^2.16.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.0.10",
43
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
44
+ "@typescript-eslint/parser": "^8.35.0",
45
+ "copyfiles": "^2.4.1",
46
+ "eslint": "^9.30.0",
47
+ "prettier": "^3.6.2",
48
+ "rimraf": "^6.0.1",
49
+ "typescript": "^5.8.3"
50
+ },
51
+ "engines": {
52
+ "node": ">=22.22"
53
+ }
54
+ }