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 +66 -0
- package/LICENSE +20 -0
- package/README.md +134 -0
- package/dist/credentials/UnoPimApi.credentials.d.ts +15 -0
- package/dist/credentials/UnoPimApi.credentials.js +131 -0
- package/dist/credentials/unopim.svg +21 -0
- package/dist/nodes/UnoPim/UnoPim.node.d.ts +9 -0
- package/dist/nodes/UnoPim/UnoPim.node.js +258 -0
- package/dist/nodes/UnoPim/UnoPim.node.json +14 -0
- package/dist/nodes/UnoPim/actions/resources.d.ts +17 -0
- package/dist/nodes/UnoPim/actions/resources.js +114 -0
- package/dist/nodes/UnoPim/methods/loadOptions.d.ts +9 -0
- package/dist/nodes/UnoPim/methods/loadOptions.js +84 -0
- package/dist/nodes/UnoPim/transport/index.d.ts +17 -0
- package/dist/nodes/UnoPim/transport/index.js +118 -0
- package/dist/nodes/UnoPim/unopim.svg +21 -0
- package/dist/nodes/UnoPimTrigger/UnoPimTrigger.node.d.ts +21 -0
- package/dist/nodes/UnoPimTrigger/UnoPimTrigger.node.js +216 -0
- package/dist/nodes/UnoPimTrigger/UnoPimTrigger.node.json +14 -0
- package/dist/nodes/UnoPimTrigger/unopim.svg +21 -0
- package/package.json +57 -0
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
|
+
}
|