n8n-nodes-unopim 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ All notable changes to `n8n-nodes-unopim` are recorded here.
4
+
5
+ The version in `package.json` and the newest heading in this file must always
6
+ match. `npm run release` bumps `package.json` on its own, so the entry below it
7
+ has to be written by hand.
8
+
9
+ ## Versions
10
+
11
+ | Version | Date | n8n | Node.js | UnoPim | UnoPim connector package |
12
+ |---|---|---|---|---|---|
13
+ | 1.0.0 | not yet published | 1.x and 2.x, tested on 2.36.7 | 22 or newer | 3.0 or newer | `webkul/n8n` 1.0.0 |
14
+
15
+ ## 1.0.0 (not yet published)
16
+
17
+ First release. Two nodes and one credential, published as a community node
18
+ package.
19
+
20
+ ### Added
21
+
22
+ - **UnoPim Trigger node.** Starts a workflow when the catalog changes. It
23
+ registers the webhook with UnoPim when the workflow is switched on and removes
24
+ it when the workflow is switched off, so there is nothing to set up in the
25
+ UnoPim admin.
26
+ - **Seventeen events.** Created, updated and deleted for products, categories,
27
+ attributes and families, plus five wildcards: one per record type and
28
+ `catalog.any` for the whole catalog. The event list is loaded from the
29
+ connected instance, so it always matches what that PIM publishes.
30
+ - **Trigger options.** Restrict deliveries to one locale or one channel, flatten
31
+ the value scopes onto a single namespace, and verify an HMAC SHA256 signature
32
+ with a shared signing secret.
33
+ - **UnoPim action node.** Eleven resources: product, configurable product,
34
+ category, attribute, attribute group, attribute family, category field,
35
+ association type, locale, channel and currency. Each supports get many, get,
36
+ create, update and delete, and the first eight also support update partially.
37
+ - **Return All.** Walks every page of a listing using UnoPim keyset pagination,
38
+ which stays fast on a large catalog where page offsets do not.
39
+ - **Filters and sorting** on every get many, using UnoPim own filter syntax, so
40
+ a scheduled workflow can ask for only what changed since its last run.
41
+ - **Custom API Call** for any endpoint the node does not cover, with the
42
+ credential already attached.
43
+ - **Loaded dropdowns.** Locales, channels, families, attributes and categories
44
+ are fetched from the instance the credential points at.
45
+ - **Agent tool support.** The action node can be attached to an AI Agent node,
46
+ so an agent can query and update the catalog directly.
47
+ - **UnoPim API credential.** Laravel Passport password grant, with a connection
48
+ test that reports the number of events the instance publishes.
49
+ - **Item linking.** Every output item is linked back to the input item that
50
+ produced it, so later nodes can trace an item to its source.
51
+ - **Continue On Fail support.** A failed item carries an `error` field instead
52
+ of a record, so a workflow can branch on it.
53
+ - **53 unit tests** run with the built in Node test runner, and a GitHub Actions
54
+ workflow that publishes to npm with provenance.
55
+
56
+ ### Notes
57
+
58
+ - Access tokens are cached in the node process and refreshed shortly before they
59
+ expire. A 401 forces one refresh and one retry.
60
+ - The credential rejects a URL that ends in `/admin`. The field takes the
61
+ application root, for example `https://demo.unopim.com`.
62
+ - A node runs once per input item. A get many feeding a single write runs that
63
+ write once per record returned, which is rarely what you want for a delete.
64
+ Aggregate the list first.
65
+ - This release needs the UnoPim side of the connector, the `webkul/n8n` package,
66
+ installed on the PIM.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2010-2026, Webkul Software
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 furnished
10
+ 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 IMPLIED,
16
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
17
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # n8n-nodes-unopim
2
+
3
+ n8n community nodes for [UnoPim](https://unopim.com), the open-source Product
4
+ Information Management system.
5
+
6
+ Two nodes ship in this package:
7
+
8
+ | Node | What it does |
9
+ |---|---|
10
+ | **UnoPim** | Reads and writes the catalog — products, categories, attributes, families and settings |
11
+ | **UnoPim Trigger** | Starts a workflow the moment the catalog changes |
12
+
13
+ [Installation](#installation) · [Credentials](#credentials) · [Operations](#operations) · [Trigger events](#trigger-events) · [Compatibility](#compatibility)
14
+
15
+ ## Installation
16
+
17
+ Follow the
18
+ [community nodes installation guide](https://docs.n8n.io/integrations/community-nodes/installation/)
19
+ and install `n8n-nodes-unopim`.
20
+
21
+ The trigger node also needs the UnoPim side of the connector installed on your
22
+ PIM. See [the connector package](https://github.com/unopim/unopim-n8n) for that
23
+ half. The action node works against a stock UnoPim install with no extra
24
+ package.
25
+
26
+ ## Credentials
27
+
28
+ UnoPim authenticates with Passport's password grant.
29
+
30
+ 1. In UnoPim, open **Configuration → Integrations → API Keys** and create a key.
31
+ 2. Copy four values from that row: Client ID, Client Secret, API Username and
32
+ API Password.
33
+ 3. In n8n, create a **UnoPim API** credential and paste them, along with your
34
+ UnoPim URL.
35
+
36
+ > The **Username** is the value UnoPim generated, in the form
37
+ > `integration+<id>@api.local`. It is not your own admin login. An admin login
38
+ > is accepted by the form but fails every request afterwards.
39
+
40
+ ## Operations
41
+
42
+ The UnoPim node covers eleven resources. Every resource supports **Get Many**,
43
+ **Get**, **Create**, **Update** and **Delete**; most also support
44
+ **Update Partially**, which sends a PATCH rather than replacing the record.
45
+
46
+ | Resource | Endpoint |
47
+ |---|---|
48
+ | Product | `/products` |
49
+ | Configurable Product | `/configurable-products` |
50
+ | Category | `/categories` |
51
+ | Attribute | `/attributes` |
52
+ | Attribute Group | `/attribute-groups` |
53
+ | Attribute Family | `/families` |
54
+ | Category Field | `/category-fields` |
55
+ | Association Type | `/association-types` |
56
+ | Locale | `/locales` |
57
+ | Channel | `/channels` |
58
+ | Currency | `/currencies` |
59
+
60
+ **Get Many** takes a **Return All** toggle. UnoPim caps a page at 100 rows, so
61
+ returning everything walks the pages for you — using keyset pagination, which
62
+ stays fast on a large catalog where page offsets do not.
63
+
64
+ You can narrow a listing with UnoPim's own filter syntax:
65
+
66
+ ```json
67
+ { "sku": [{ "operator": "IN", "value": ["SHIRT-01", "SHIRT-02"] }] }
68
+ ```
69
+
70
+ Dropdowns for locales, channels, families, attributes and categories are loaded
71
+ live from the connected instance, so they always match what that PIM actually
72
+ has.
73
+
74
+ ### Use with an AI agent
75
+
76
+ The UnoPim node is available as a tool, so an AI Agent node can query and update
77
+ the catalog directly — "find every product missing a description in French and
78
+ fill it in" becomes one agent step rather than a hand-built branch.
79
+
80
+ ## Trigger events
81
+
82
+ The trigger registers a webhook with UnoPim when the workflow is activated and
83
+ removes it when the workflow is switched off. Nothing polls.
84
+
85
+ | Entity | Events |
86
+ |---|---|
87
+ | Product | created · updated · deleted |
88
+ | Category | created · updated · deleted |
89
+ | Attribute | created · updated · deleted |
90
+ | Attribute Family | created · updated · deleted |
91
+
92
+ Five wildcards cover the case where a workflow cares that *something* changed
93
+ rather than exactly what: `product.any`, `category.any`, `attribute.any`,
94
+ `family.any` and `catalog.any`. Every payload carries `entity` and `reference`,
95
+ so a workflow listening on `catalog.any` can tell a product from a family.
96
+
97
+ ### Options
98
+
99
+ - **Locale** and **Channel** narrow the delivered values to one scope.
100
+ - **Flatten Values** collapses the attribute value scopes onto one level
101
+ (`price__default__en_US`). It is off by default — n8n reads nested JSON
102
+ natively, and the nested shape is easier to walk in an expression.
103
+ - **Signing Secret** makes UnoPim sign each delivery. A delivery whose
104
+ signature does not match is rejected.
105
+
106
+ ### Notes
107
+
108
+ - UnoPim writes a product row and then saves its values, so creating a product
109
+ through the API fires both `product.created` and `product.updated`. A
110
+ workflow with both triggers enabled runs twice for one new product.
111
+ - The UnoPim side delivers on a dedicated queue. Without a queue worker running
112
+ on that queue, no trigger ever fires.
113
+ - If your n8n instance is not reachable from UnoPim, deliveries fail and the
114
+ subscription is deactivated after several consecutive failures. It
115
+ reactivates by itself the next time the workflow is switched on.
116
+
117
+ ## Compatibility
118
+
119
+ - n8n 1.x and 2.x, tested on 2.36.7
120
+ - Node.js 22 or newer
121
+ - UnoPim 3.0 or newer
122
+
123
+ ## Changelog
124
+
125
+ Release history is in [CHANGELOG.md](CHANGELOG.md).
126
+
127
+ ## Resources
128
+
129
+ - [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
130
+ - [UnoPim documentation](https://docs.unopim.com/)
131
+
132
+ ## License
133
+
134
+ [MIT](LICENSE)
@@ -0,0 +1,15 @@
1
+ import type { IAuthenticateGeneric, ICredentialDataDecryptedObject, ICredentialTestRequest, ICredentialType, IHttpRequestHelper, INodeProperties } from 'n8n-workflow';
2
+ export declare class UnoPimApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ icon: "file:unopim.svg";
6
+ documentationUrl: string;
7
+ properties: INodeProperties[];
8
+ preAuthentication(this: IHttpRequestHelper, credentials: ICredentialDataDecryptedObject): Promise<{
9
+ sessionToken: string;
10
+ }>;
11
+ authenticate: IAuthenticateGeneric;
12
+ supportedNodes: string[];
13
+ test: ICredentialTestRequest;
14
+ }
15
+ export declare function normaliseBaseUrl(baseUrl: string): string;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnoPimApi = void 0;
4
+ exports.normaliseBaseUrl = normaliseBaseUrl;
5
+ const REJECTED = 'Those credentials were rejected. Copy all four values from the same row under ' +
6
+ 'Configuration > Integrations > API Keys. The Username is the generated API username ' +
7
+ 'in the form integration+<id>@api.local — not your own admin login.';
8
+ class UnoPimApi {
9
+ name = 'unoPimApi';
10
+ displayName = 'UnoPim API';
11
+ icon = 'file:unopim.svg';
12
+ documentationUrl = 'https://docs.unopim.com/';
13
+ properties = [
14
+ {
15
+ displayName: 'UnoPim URL',
16
+ name: 'baseUrl',
17
+ type: 'string',
18
+ default: '',
19
+ placeholder: 'https://demo.unopim.com',
20
+ required: true,
21
+ description: 'Root URL of your UnoPim instance, without a trailing slash',
22
+ },
23
+ {
24
+ displayName: 'Client ID',
25
+ name: 'clientId',
26
+ type: 'string',
27
+ default: '',
28
+ required: true,
29
+ description: 'From Configuration > Integrations > API Keys in UnoPim. Create a key, then copy its Client ID.',
30
+ },
31
+ {
32
+ displayName: 'Client Secret',
33
+ name: 'clientSecret',
34
+ type: 'string',
35
+ typeOptions: { password: true },
36
+ default: '',
37
+ required: true,
38
+ description: 'The Client Secret on the same row as the Client ID',
39
+ },
40
+ {
41
+ displayName: 'Username',
42
+ name: 'username',
43
+ type: 'string',
44
+ default: '',
45
+ required: true,
46
+ placeholder: 'integration+1@api.local',
47
+ description: 'The API username on the same row, generated by UnoPim in the form integration+<id>@api.local. This is not your own admin login.',
48
+ },
49
+ {
50
+ displayName: 'Password',
51
+ name: 'password',
52
+ type: 'string',
53
+ typeOptions: { password: true },
54
+ default: '',
55
+ required: true,
56
+ description: 'The API password on the same row, not your own admin password',
57
+ },
58
+ {
59
+ displayName: 'Session Token',
60
+ name: 'sessionToken',
61
+ type: 'hidden',
62
+ typeOptions: { password: true },
63
+ default: '',
64
+ },
65
+ ];
66
+ async preAuthentication(credentials) {
67
+ const baseUrl = normaliseBaseUrl(credentials.baseUrl);
68
+ let response;
69
+ try {
70
+ response = (await this.helpers.httpRequest({
71
+ method: 'POST',
72
+ url: `${baseUrl}/oauth/token`,
73
+ headers: {
74
+ Accept: 'application/json',
75
+ 'Content-Type': 'application/x-www-form-urlencoded',
76
+ },
77
+ body: new URLSearchParams({
78
+ grant_type: 'password',
79
+ client_id: String(credentials.clientId ?? ''),
80
+ client_secret: String(credentials.clientSecret ?? ''),
81
+ username: String(credentials.username ?? ''),
82
+ password: String(credentials.password ?? ''),
83
+ scope: '',
84
+ }).toString(),
85
+ json: true,
86
+ }));
87
+ }
88
+ catch {
89
+ throw new Error(REJECTED);
90
+ }
91
+ if (!response?.access_token) {
92
+ throw new Error('UnoPim did not return an access token. Check that the UnoPim URL points at the application root and that /oauth/token is reachable there.');
93
+ }
94
+ return { sessionToken: response.access_token };
95
+ }
96
+ authenticate = {
97
+ type: 'generic',
98
+ properties: {
99
+ headers: {
100
+ Authorization: '=Bearer {{$credentials.sessionToken}}',
101
+ Accept: 'application/json',
102
+ },
103
+ },
104
+ };
105
+ supportedNodes = ['unoPim', 'unoPimTrigger'];
106
+ test = {
107
+ request: {
108
+ baseURL: '={{$credentials.baseUrl.replace(/\\/+$/, "")}}',
109
+ url: '/api/v1/rest/n8n/me',
110
+ },
111
+ };
112
+ }
113
+ exports.UnoPimApi = UnoPimApi;
114
+ function normaliseBaseUrl(baseUrl) {
115
+ const trimmed = (baseUrl ?? '').trim().replace(/\/+$/, '');
116
+ if (!/^https?:\/\//i.test(trimmed)) {
117
+ throw new Error('The UnoPim URL must start with http:// or https://.');
118
+ }
119
+ const url = new URL(trimmed);
120
+ if (url.username || url.password) {
121
+ throw new Error('Remove the username and password from the UnoPim URL. Enter them in the fields below instead.');
122
+ }
123
+ if (url.search || url.hash) {
124
+ throw new Error('The UnoPim URL must not contain a query string or fragment. Use just the address, for example https://demo.unopim.com.');
125
+ }
126
+ if (/\/admin$/i.test(url.pathname)) {
127
+ throw new Error('Remove "/admin" from the end of the UnoPim URL. This field takes the application root, ' +
128
+ `for example ${trimmed.replace(/\/admin$/i, '') || 'https://demo.unopim.com'}, not the admin panel address.`);
129
+ }
130
+ return trimmed;
131
+ }
@@ -0,0 +1,21 @@
1
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <style>
3
+ .uno-fav-quad { fill: #414361; }
4
+ @media (prefers-color-scheme: dark) {
5
+ .uno-fav-quad { fill: #F4F2FE; }
6
+ }
7
+ </style>
8
+ <g clip-path="url(#clip0_200_7482)">
9
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.33333 0C2.38782 0 0 2.38781 0 5.33333V12H8V8H12V0H5.33333Z" fill="#8B5CF6"/>
10
+ <path d="M8 4C8 1.79086 9.79085 0 12 0C14.2091 0 16 1.79086 16 4C16 6.20915 14.2091 8 12 8H8V4Z" fill="#4DCCBD"/>
11
+ <path class="uno-fav-quad" d="M8 12C8 14.2091 6.20915 16 4 16C1.79086 16 -1.93129e-07 14.2091 0 12C1.93131e-07 9.79085 1.79086 8 4 8H8V12Z" fill="#414361"/>
12
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.33333 0C2.38782 0 0 2.38781 0 5.33333V12H8V8H12V0H5.33333Z" fill="#8B5CF6"/>
13
+ <path d="M8 4C8 1.79086 9.79085 0 12 0C14.2091 0 16 1.79086 16 4C16 6.20915 14.2091 8 12 8H8V4Z" fill="#4DCCBD"/>
14
+ <path class="uno-fav-quad" d="M8 12C8 14.2091 6.20915 16 4 16C1.79086 16 -1.93129e-07 14.2091 0 12C1.93131e-07 9.79085 1.79086 8 4 8H8V12Z" fill="#414361"/>
15
+ </g>
16
+ <defs>
17
+ <clipPath id="clip0_200_7482">
18
+ <rect width="16" height="16" fill="white"/>
19
+ </clipPath>
20
+ </defs>
21
+ </svg>
@@ -0,0 +1,9 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ import * as loadOptions from './methods/loadOptions';
3
+ export declare class UnoPim implements INodeType {
4
+ description: INodeTypeDescription;
5
+ methods: {
6
+ loadOptions: typeof loadOptions;
7
+ };
8
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
9
+ }
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.UnoPim = void 0;
37
+ const n8n_workflow_1 = require("n8n-workflow");
38
+ const resources_1 = require("./actions/resources");
39
+ const loadOptions = __importStar(require("./methods/loadOptions"));
40
+ const transport_1 = require("./transport");
41
+ const OPERATIONS_NEEDING_IDENTIFIER = ['get', 'update', 'patch', 'delete'];
42
+ const OPERATIONS_NEEDING_BODY = ['create', 'update', 'patch'];
43
+ function operationProperty() {
44
+ return resources_1.RESOURCES.map((resource) => ({
45
+ displayName: 'Operation',
46
+ name: 'operation',
47
+ type: 'options',
48
+ noDataExpression: true,
49
+ displayOptions: {
50
+ show: { resource: [resource.value] },
51
+ },
52
+ options: resource.operations.map((operation) => ({
53
+ name: resources_1.OPERATION_LABELS[operation].name,
54
+ value: operation,
55
+ action: `${resources_1.OPERATION_LABELS[operation].action} in ${resource.name.toLowerCase()}`,
56
+ })),
57
+ default: resource.operations[0],
58
+ }));
59
+ }
60
+ function identifierProperty() {
61
+ return resources_1.RESOURCES.map((resource) => ({
62
+ displayName: resource.identifier,
63
+ name: 'identifier',
64
+ type: 'string',
65
+ required: true,
66
+ default: '',
67
+ displayOptions: {
68
+ show: {
69
+ resource: [resource.value],
70
+ operation: resource.operations.filter((operation) => OPERATIONS_NEEDING_IDENTIFIER.includes(operation)),
71
+ },
72
+ },
73
+ description: `The ${resource.identifier.toLowerCase()} of the ${resource.name.toLowerCase()} to act on`,
74
+ }));
75
+ }
76
+ class UnoPim {
77
+ description = {
78
+ displayName: 'UnoPim',
79
+ name: 'unoPim',
80
+ icon: 'file:unopim.svg',
81
+ group: ['transform'],
82
+ version: 1,
83
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
84
+ description: 'Read and write the UnoPim catalog',
85
+ defaults: {
86
+ name: 'UnoPim',
87
+ },
88
+ usableAsTool: true,
89
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
90
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
91
+ credentials: [
92
+ {
93
+ name: 'unoPimApi',
94
+ required: true,
95
+ },
96
+ ],
97
+ properties: [
98
+ {
99
+ displayName: 'Resource',
100
+ name: 'resource',
101
+ type: 'options',
102
+ noDataExpression: true,
103
+ options: resources_1.RESOURCE_OPTIONS,
104
+ default: 'product',
105
+ },
106
+ ...operationProperty(),
107
+ ...identifierProperty(),
108
+ {
109
+ displayName: 'Data',
110
+ name: 'data',
111
+ type: 'json',
112
+ default: '{}',
113
+ required: true,
114
+ displayOptions: {
115
+ show: {
116
+ operation: OPERATIONS_NEEDING_BODY,
117
+ },
118
+ },
119
+ description: 'The record body, as UnoPim\'s API expects it. For a product this is the object carrying sku, family, status and values.',
120
+ },
121
+ {
122
+ displayName: 'Return All',
123
+ name: 'returnAll',
124
+ type: 'boolean',
125
+ default: false,
126
+ displayOptions: {
127
+ show: { operation: ['getMany'] },
128
+ },
129
+ description: 'Whether to return all results or only up to a given limit',
130
+ },
131
+ {
132
+ displayName: 'Limit',
133
+ name: 'limit',
134
+ type: 'number',
135
+ typeOptions: { minValue: 1 },
136
+ default: 50,
137
+ displayOptions: {
138
+ show: {
139
+ operation: ['getMany'],
140
+ returnAll: [false],
141
+ },
142
+ },
143
+ description: 'Max number of results to return',
144
+ },
145
+ {
146
+ displayName: 'Options',
147
+ name: 'options',
148
+ type: 'collection',
149
+ placeholder: 'Add option',
150
+ default: {},
151
+ displayOptions: {
152
+ show: { operation: ['getMany'] },
153
+ },
154
+ options: [
155
+ {
156
+ displayName: 'Filters (JSON)',
157
+ name: 'filters',
158
+ type: 'json',
159
+ default: '{}',
160
+ description: 'UnoPim filter object, for example {"sku":[{"operator":"IN","value":["A","B"]}]}',
161
+ },
162
+ {
163
+ displayName: 'Sort',
164
+ name: 'sort',
165
+ type: 'string',
166
+ default: '',
167
+ placeholder: 'updated_at',
168
+ description: 'Column to sort by',
169
+ },
170
+ ],
171
+ },
172
+ ],
173
+ };
174
+ methods = {
175
+ loadOptions,
176
+ };
177
+ async execute() {
178
+ const items = this.getInputData();
179
+ const returnData = [];
180
+ const resourceName = this.getNodeParameter('resource', 0);
181
+ const operation = this.getNodeParameter('operation', 0);
182
+ const resource = (0, resources_1.resourceByValue)(resourceName);
183
+ for (let i = 0; i < items.length; i++) {
184
+ try {
185
+ const rows = await runOperation.call(this, resource.endpoint, operation, i);
186
+ returnData.push(...rows.map((row) => ({
187
+ json: row,
188
+ pairedItem: { item: i },
189
+ })));
190
+ }
191
+ catch (error) {
192
+ if (this.continueOnFail()) {
193
+ returnData.push({
194
+ json: { error: error.message },
195
+ pairedItem: { item: i },
196
+ });
197
+ continue;
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+ return [returnData];
203
+ }
204
+ }
205
+ exports.UnoPim = UnoPim;
206
+ async function runOperation(endpoint, operation, itemIndex) {
207
+ if (operation === 'getMany') {
208
+ return listRecords.call(this, endpoint, itemIndex);
209
+ }
210
+ const identifier = OPERATIONS_NEEDING_IDENTIFIER.includes(operation)
211
+ ? encodeURIComponent(this.getNodeParameter('identifier', itemIndex))
212
+ : '';
213
+ const body = OPERATIONS_NEEDING_BODY.includes(operation)
214
+ ? parseJsonParameter.call(this, 'data', itemIndex)
215
+ : {};
216
+ const method = {
217
+ create: 'POST',
218
+ update: 'PUT',
219
+ patch: 'PATCH',
220
+ delete: 'DELETE',
221
+ get: 'GET',
222
+ getMany: 'GET',
223
+ }[operation];
224
+ const path = operation === 'create' ? endpoint : `${endpoint}/${identifier}`;
225
+ const response = await transport_1.unopimApiRequest.call(this, method, path, body);
226
+ return [response ?? {}];
227
+ }
228
+ async function listRecords(endpoint, itemIndex) {
229
+ const options = this.getNodeParameter('options', itemIndex, {});
230
+ const qs = {};
231
+ if (options.sort) {
232
+ qs.sort = options.sort;
233
+ }
234
+ const filters = parseJsonParameter.call(this, 'options.filters', itemIndex, options.filters);
235
+ if (Object.keys(filters).length > 0) {
236
+ qs.filters = JSON.stringify(filters);
237
+ }
238
+ if (this.getNodeParameter('returnAll', itemIndex)) {
239
+ return transport_1.unopimApiRequestAllItems.call(this, endpoint, qs);
240
+ }
241
+ const limit = this.getNodeParameter('limit', itemIndex);
242
+ return transport_1.unopimApiRequestLimited.call(this, endpoint, limit, qs);
243
+ }
244
+ function parseJsonParameter(name, itemIndex, raw) {
245
+ const value = raw ?? this.getNodeParameter(name, itemIndex, '{}');
246
+ if (value === undefined || value === null || value === '') {
247
+ return {};
248
+ }
249
+ if (typeof value === 'object') {
250
+ return value;
251
+ }
252
+ try {
253
+ return JSON.parse(value);
254
+ }
255
+ catch {
256
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `The value in "${name}" is not valid JSON. Fix the JSON, or pass an object from a previous node.`, { itemIndex });
257
+ }
258
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "node": "n8n-nodes-unopim.unoPim",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["Data & Storage", "Marketing"],
6
+ "resources": {
7
+ "credentialDocumentation": [
8
+ { "url": "https://docs.unopim.com/" }
9
+ ],
10
+ "primaryDocumentation": [
11
+ { "url": "https://github.com/unopim/n8n-nodes-unopim" }
12
+ ]
13
+ }
14
+ }