n8n-nodes-idb2b 3.2.1 → 3.2.4

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 CHANGED
@@ -75,10 +75,14 @@ npm install n8n-nodes-idb2b
75
75
 
76
76
  Required:
77
77
  - **Name**: Contact full name
78
- - **Email**: Valid email address
79
78
 
80
79
  Optional (under Additional Fields):
81
- - Phone Number, User ID, Lead ID, Favorites, Tags
80
+ - **Phone Number**: Can be blank when you only have profile data
81
+ - **Email**: Can be blank
82
+ - **Job Title**, **Owner ID**, **Company ID**, **Status ID**, **Source ID**
83
+ - **LinkedIn URL**: Added as a social link payload
84
+ - **Social Links**: Add one or more social profiles
85
+ - **Tags**
82
86
 
83
87
  ### Create Company
84
88
 
@@ -103,6 +107,7 @@ Webhook → IDB2B Create Contact
103
107
  - Name: {{ $json.name }}
104
108
  - Email: {{ $json.email }}
105
109
  - Phone: {{ $json.phone }}
110
+ - Additional Fields.LinkedIn URL: {{ $json.linkedin_url }}
106
111
  ```
107
112
 
108
113
  ### Paginate through all contacts
@@ -58,7 +58,6 @@ class IDB2B {
58
58
  };
59
59
  }
60
60
  async execute() {
61
- var _a;
62
61
  const items = this.getInputData();
63
62
  const returnData = [];
64
63
  const errorHandler = errorHandler_1.defaultErrorHandler;
@@ -104,17 +103,16 @@ class IDB2B {
104
103
  method = "POST";
105
104
  endpoint = constants_1.ENDPOINTS.CONTACTS;
106
105
  const name = this.getNodeParameter("name", i);
107
- const email = this.getNodeParameter("email", i, "") || "";
108
- const phone_number = this.getNodeParameter("phone_number", i, "") || "";
106
+ const email = this.getNodeParameter("email", i);
107
+ const phone_number = this.getNodeParameter("phone_number", i, "");
109
108
  const additionalFields = this.getNodeParameter("additionalFields", i, {});
110
109
  const validation = validator.validateContactData(name, email, phone_number, false);
111
110
  if (!validation.isValid) {
112
111
  throw new Error(validation.error);
113
112
  }
114
- const contactData = Object.assign({ name, phone_number: phone_number || "" }, additionalFields);
115
- if (email)
116
- contactData.email = email;
117
- body = (0, common_1.buildContactRequestBody)(contactData);
113
+ body = (0, common_1.buildContactRequestBody)(Object.assign({ name,
114
+ email,
115
+ phone_number }, additionalFields));
118
116
  initialBody = body;
119
117
  }
120
118
  else if (operation === "update") {
@@ -251,17 +249,9 @@ class IDB2B {
251
249
  });
252
250
  continue;
253
251
  }
254
- let errorMsg = error.message;
255
- if (error instanceof Error &&
256
- "response" in error &&
257
- ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data)) {
258
- try {
259
- const apiError = error.response.data;
260
- errorMsg += ` | API response: ${typeof apiError === "string" ? apiError : JSON.stringify(apiError)}`;
261
- }
262
- catch (_b) { }
263
- }
264
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), new Error(errorMsg), { itemIndex: i });
252
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, {
253
+ itemIndex: i,
254
+ });
265
255
  }
266
256
  }
267
257
  return [returnData];
@@ -191,6 +191,13 @@ exports.contactFields = [
191
191
  default: '',
192
192
  description: 'UUID of the user who owns this contact',
193
193
  },
194
+ {
195
+ displayName: 'LinkedIn URL',
196
+ name: 'linkedin_url',
197
+ type: 'string',
198
+ default: '',
199
+ description: 'LinkedIn profile URL to attach as a social link',
200
+ },
194
201
  {
195
202
  displayName: 'Tags',
196
203
  name: 'tags',
@@ -217,6 +224,56 @@ exports.contactFields = [
217
224
  },
218
225
  ],
219
226
  },
227
+ {
228
+ displayName: 'Social Links',
229
+ name: 'socials',
230
+ type: 'fixedCollection',
231
+ typeOptions: {
232
+ multipleValues: true,
233
+ },
234
+ default: {},
235
+ placeholder: 'Add Social Link',
236
+ description: 'Social links to associate with the contact',
237
+ options: [
238
+ {
239
+ name: 'social',
240
+ displayName: 'Social',
241
+ values: [
242
+ {
243
+ displayName: 'Platform',
244
+ name: 'social_type',
245
+ type: 'options',
246
+ default: 'linkedin',
247
+ options: [
248
+ { name: 'LinkedIn', value: 'linkedin' },
249
+ { name: 'Instagram', value: 'instagram' },
250
+ { name: 'Facebook', value: 'facebook' },
251
+ { name: 'Twitter / X', value: 'twitter' },
252
+ { name: 'GitHub', value: 'github' },
253
+ { name: 'YouTube', value: 'youtube' },
254
+ { name: 'TikTok', value: 'tiktok' },
255
+ { name: 'Website', value: 'website' },
256
+ ],
257
+ },
258
+ {
259
+ displayName: 'Value',
260
+ name: 'social_identifier',
261
+ type: 'string',
262
+ default: '',
263
+ required: true,
264
+ description: 'Username, slug, or unique social identifier',
265
+ },
266
+ {
267
+ displayName: 'Social URL',
268
+ name: 'social_id',
269
+ type: 'string',
270
+ default: '',
271
+ description: 'Optional full URL. If empty, the node will derive it from the identifier when possible.',
272
+ },
273
+ ],
274
+ },
275
+ ],
276
+ },
220
277
  ],
221
278
  },
222
279
  {
@@ -58,7 +58,7 @@ class ContactHandler {
58
58
  */
59
59
  async create(params) {
60
60
  // Validate required fields
61
- const validation = this.validator.validateContactData(params.data.name, params.data.email, params.data.phone_number, false);
61
+ const validation = this.validator.validateContactData(params.data.name, params.data.email, params.data.phone_number);
62
62
  if (!validation.isValid) {
63
63
  throw new Error(validation.error);
64
64
  }
@@ -90,6 +90,14 @@ function processResponse(response, operation, requestBody) {
90
90
  requestBody) {
91
91
  processed = Object.assign(Object.assign({}, response), { data: Object.assign(Object.assign({}, requestBody), { created: true, status: "success", _note: "Server did not return the created entity. Fields like id and timestamps are unavailable." }) });
92
92
  }
93
+ // Merge socials from request body into create/update response data
94
+ // The API does not return socials in the response, so we add them back
95
+ if ((operation === "create" || operation === "update") &&
96
+ response.message === "success" &&
97
+ response.data &&
98
+ (requestBody === null || requestBody === void 0 ? void 0 : requestBody.socials)) {
99
+ processed = Object.assign(Object.assign({}, processed), { data: Object.assign(Object.assign({}, processed.data), { socials: requestBody.socials }) });
100
+ }
93
101
  // Standardize delete operation response
94
102
  if (operation === "delete") {
95
103
  processed = { deleted: true };
@@ -112,7 +120,33 @@ function buildContactRequestBody(data, includesPhone = true) {
112
120
  "status_id",
113
121
  "source_id",
114
122
  "owner_id",
123
+ "socials",
115
124
  ]);
125
+ const buildSocialId = (socialType, socialIdentifier) => {
126
+ const trimmedIdentifier = socialIdentifier.trim();
127
+ const normalizedType = socialType.trim().toLowerCase();
128
+ if (trimmedIdentifier.startsWith("http://") ||
129
+ trimmedIdentifier.startsWith("https://")) {
130
+ return trimmedIdentifier;
131
+ }
132
+ if (normalizedType === "linkedin") {
133
+ return `https://www.linkedin.com/in/${trimmedIdentifier}`;
134
+ }
135
+ return trimmedIdentifier;
136
+ };
137
+ const extractLinkedInIdentifier = (value) => {
138
+ const trimmedValue = value.trim();
139
+ try {
140
+ const url = new URL(trimmedValue);
141
+ const parts = url.pathname.split("/").filter(Boolean);
142
+ const profileIndex = parts.findIndex((part) => ["in", "company"].includes(part));
143
+ const candidate = profileIndex >= 0 ? parts[profileIndex + 1] : parts[parts.length - 1];
144
+ return candidate || trimmedValue;
145
+ }
146
+ catch (_a) {
147
+ return trimmedValue.replace(/^@/, "");
148
+ }
149
+ };
116
150
  // Always include name and email if provided
117
151
  if (data.name !== undefined) {
118
152
  body.name = typeof data.name === "string" ? data.name.trim() : data.name;
@@ -126,6 +160,9 @@ function buildContactRequestBody(data, includesPhone = true) {
126
160
  if (includesPhone) {
127
161
  body.phone_number = (data.phone_number != null && data.phone_number !== "") ? String(data.phone_number) : "";
128
162
  }
163
+ if (includesPhone && data.phone_number === "") {
164
+ body.phone_number = "";
165
+ }
129
166
  // Handle tags specially - convert from fixedCollection format
130
167
  if (data.tags && data.tags.tag && Array.isArray(data.tags.tag)) {
131
168
  body.tags = data.tags.tag.map((tag) => ({
@@ -139,6 +176,39 @@ function buildContactRequestBody(data, includesPhone = true) {
139
176
  name: typeof tag.name === "string" ? tag.name.trim() : tag.name,
140
177
  }));
141
178
  }
179
+ const socials = [];
180
+ if (typeof data.linkedin_url === "string" && data.linkedin_url.trim()) {
181
+ const socialId = data.linkedin_url.trim();
182
+ socials.push({
183
+ social_type: "linkedin",
184
+ social_id: socialId,
185
+ social_identifier: extractLinkedInIdentifier(socialId),
186
+ });
187
+ }
188
+ if (data.socials && data.socials.social && Array.isArray(data.socials.social)) {
189
+ data.socials.social.forEach((social) => {
190
+ if (!(social === null || social === void 0 ? void 0 : social.social_type) || !(social === null || social === void 0 ? void 0 : social.social_identifier)) {
191
+ return;
192
+ }
193
+ const socialType = typeof social.social_type === "string"
194
+ ? social.social_type.trim().toLowerCase()
195
+ : social.social_type;
196
+ const socialIdentifier = typeof social.social_identifier === "string"
197
+ ? social.social_identifier.trim()
198
+ : social.social_identifier;
199
+ const providedSocialId = typeof social.social_id === "string" ? social.social_id.trim() : "";
200
+ socials.push({
201
+ social_type: socialType,
202
+ social_id: providedSocialId || buildSocialId(String(socialType), String(socialIdentifier)),
203
+ social_identifier: socialType === "linkedin"
204
+ ? extractLinkedInIdentifier(String(socialIdentifier))
205
+ : socialIdentifier,
206
+ });
207
+ });
208
+ }
209
+ if (socials.length > 0) {
210
+ body.socials = socials;
211
+ }
142
212
  Object.entries(data).forEach(([rawKey, rawValue]) => {
143
213
  var _a;
144
214
  if ([
@@ -146,6 +216,8 @@ function buildContactRequestBody(data, includesPhone = true) {
146
216
  "email",
147
217
  "phone_number",
148
218
  "tags",
219
+ "linkedin_url",
220
+ "socials",
149
221
  ].includes(rawKey)) {
150
222
  return;
151
223
  }
@@ -60,9 +60,9 @@ class DataValidator {
60
60
  }
61
61
  }
62
62
  if (requirePhone &&
63
- (!phoneNumber ||
64
- typeof phoneNumber !== "string" ||
65
- phoneNumber.trim().length === 0)) {
63
+ (phoneNumber === undefined ||
64
+ phoneNumber === null ||
65
+ typeof phoneNumber !== "string")) {
66
66
  return {
67
67
  isValid: false,
68
68
  error: "Contact phone number is required",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-idb2b",
3
- "version": "3.2.1",
3
+ "version": "3.2.4",
4
4
  "description": "n8n community node for IDB2B - WhatsApp AI Agents",
5
5
  "main": "index.js",
6
6
  "scripts": {