n8n-nodes-steyi-ss 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.
Files changed (25) hide show
  1. package/README.md +192 -0
  2. package/dist/credentials/SteyiSmartsheetCreds.credentials.d.ts +9 -0
  3. package/dist/credentials/SteyiSmartsheetCreds.credentials.js +38 -0
  4. package/dist/nodes/SteyiSmartsheet/SteyiGenericFunction.d.ts +3 -0
  5. package/dist/nodes/SteyiSmartsheet/SteyiGenericFunction.js +58 -0
  6. package/dist/nodes/SteyiSmartsheet/SteyiSmartsheet.node.d.ts +19 -0
  7. package/dist/nodes/SteyiSmartsheet/SteyiSmartsheet.node.js +1988 -0
  8. package/dist/nodes/SteyiSmartsheet/SteyiSmartsheetApi.d.ts +24 -0
  9. package/dist/nodes/SteyiSmartsheet/SteyiSmartsheetApi.js +174 -0
  10. package/dist/nodes/SteyiSmartsheet/SteyiSmartsheetTrigger.node.d.ts +17 -0
  11. package/dist/nodes/SteyiSmartsheet/SteyiSmartsheetTrigger.node.js +173 -0
  12. package/dist/nodes/SteyiSmartsheet/executors/Admin.d.ts +2 -0
  13. package/dist/nodes/SteyiSmartsheet/executors/Admin.js +180 -0
  14. package/dist/nodes/SteyiSmartsheet/executors/Columns.d.ts +2 -0
  15. package/dist/nodes/SteyiSmartsheet/executors/Columns.js +53 -0
  16. package/dist/nodes/SteyiSmartsheet/executors/Reports.d.ts +2 -0
  17. package/dist/nodes/SteyiSmartsheet/executors/Reports.js +27 -0
  18. package/dist/nodes/SteyiSmartsheet/executors/Rows.d.ts +2 -0
  19. package/dist/nodes/SteyiSmartsheet/executors/Rows.js +845 -0
  20. package/dist/nodes/SteyiSmartsheet/executors/Sheets.d.ts +2 -0
  21. package/dist/nodes/SteyiSmartsheet/executors/Sheets.js +85 -0
  22. package/dist/nodes/SteyiSmartsheet/executors/Webhooks.d.ts +2 -0
  23. package/dist/nodes/SteyiSmartsheet/executors/Webhooks.js +67 -0
  24. package/dist/nodes/SteyiSmartsheet/steyi-smartsheet.svg +6 -0
  25. package/package.json +56 -0
@@ -0,0 +1,845 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeRowOperation = void 0;
4
+ const SteyiGenericFunction_1 = require("../SteyiGenericFunction");
5
+ const SteyiSmartsheetApi_1 = require("../SteyiSmartsheetApi");
6
+ // Helper function to handle attachment uploads
7
+ async function handleAttachment(att, sheetId, rowId, itemIndex) {
8
+ if (att.attachmentType === 'FILE') {
9
+ // For FILE type, upload binary data
10
+ let binaryData;
11
+ let fileName = att.name;
12
+ let mimeType = 'application/octet-stream';
13
+ // Check if URL is a binary data reference
14
+ if (att.url && att.url !== '') {
15
+ // Get binary data by property name (similar to HTTP Request node's "Input Data Field Name")
16
+ const binaryPropertyName = att.url.trim(); // Trim whitespace
17
+ // Use n8n's helper to get binary data buffer - this handles all conversion automatically
18
+ // Similar to how HTTP Request node handles "n8n Binary File" body content type
19
+ try {
20
+ const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
21
+ const BufferClass = eval('Buffer');
22
+ // Check the structure - it might be the buffer directly or wrapped in an object
23
+ if (BufferClass && BufferClass.isBuffer && BufferClass.isBuffer(binaryDataBuffer)) {
24
+ // It's a Buffer directly
25
+ binaryData = binaryDataBuffer;
26
+ // Get metadata from the item
27
+ const items = this.getInputData();
28
+ const item = items[itemIndex];
29
+ const binary = item.binary?.[binaryPropertyName];
30
+ fileName = att.name || binary?.fileName || 'attachment';
31
+ mimeType = binary?.mimeType || 'application/octet-stream';
32
+ }
33
+ else if (binaryDataBuffer && binaryDataBuffer.data) {
34
+ // It's wrapped in an object with data property
35
+ binaryData = binaryDataBuffer.data;
36
+ fileName = att.name || binaryDataBuffer.fileName || 'attachment';
37
+ mimeType = binaryDataBuffer.mimeType || 'application/octet-stream';
38
+ }
39
+ else {
40
+ throw new Error(`Unexpected binary data structure: ${typeof binaryDataBuffer}. Expected Buffer or object with data property.`);
41
+ }
42
+ // Validate the buffer
43
+ if (!binaryData) {
44
+ throw new Error(`Invalid buffer returned. Type: ${typeof binaryData}`);
45
+ }
46
+ if (!BufferClass || !BufferClass.isBuffer || !BufferClass.isBuffer(binaryData)) {
47
+ throw new Error(`Data is not a Buffer. Type: ${typeof binaryData}`);
48
+ }
49
+ if (binaryData.length === 0) {
50
+ throw new Error(`Buffer is empty. Binary property "${binaryPropertyName}" may not contain valid data.`);
51
+ }
52
+ }
53
+ catch (error) {
54
+ // Fallback: manually get binary data from item
55
+ const items = this.getInputData();
56
+ const item = items[itemIndex];
57
+ if (!item.binary) {
58
+ const availableProps = 'none';
59
+ throw new Error(`No binary data found in input item. Available binary properties: ${availableProps}. Make sure the previous node outputs binary data.`);
60
+ }
61
+ if (!item.binary[binaryPropertyName]) {
62
+ // List available binary property names to help user
63
+ const availableProps = Object.keys(item.binary).join(', ');
64
+ throw new Error(`Binary property "${binaryPropertyName}" not found. Available binary properties: ${availableProps || 'none'}. Make sure you're using the correct property name from the previous node.`);
65
+ }
66
+ const binary = item.binary[binaryPropertyName];
67
+ // Get the base64 data
68
+ let base64Data = binary.data;
69
+ // Remove data URL prefix if present (e.g., "data:application/pdf;base64,")
70
+ if (base64Data.includes(',')) {
71
+ base64Data = base64Data.split(',')[1];
72
+ }
73
+ // Clean the base64 string - remove whitespace and newlines
74
+ const base64String = base64Data.replace(/\s/g, '');
75
+ // Convert base64 to Buffer - this is critical for proper file upload
76
+ const BufferClass = eval('Buffer');
77
+ if (!BufferClass || typeof BufferClass.from !== 'function') {
78
+ throw new Error('Buffer is not available. Cannot convert base64 to binary data.');
79
+ }
80
+ // Create Buffer from base64 string
81
+ binaryData = BufferClass.from(base64String, 'base64');
82
+ // Verify we have valid binary data
83
+ if (!binaryData || !BufferClass.isBuffer(binaryData)) {
84
+ throw new Error(`Failed to create Buffer from base64 data. Base64 length: ${base64String.length}`);
85
+ }
86
+ // Get file metadata
87
+ fileName = att.name || binary.fileName || 'attachment';
88
+ mimeType = binary.mimeType || 'application/octet-stream';
89
+ }
90
+ // Additional validation - check buffer size
91
+ if (!binaryData || binaryData.length === 0) {
92
+ throw new Error('Binary data buffer is empty. Cannot upload file.');
93
+ }
94
+ // Upload file using raw binary data
95
+ // Ensure rowId is a number
96
+ const rowIdNum = typeof rowId === 'number' ? rowId : parseInt(String(rowId), 10);
97
+ if (isNaN(rowIdNum)) {
98
+ throw new Error(`Invalid row ID: ${rowId}`);
99
+ }
100
+ // Check if this should be added to a proof (use /proofs endpoint) or regular attachment (use /attachments endpoint)
101
+ const isProof = att.attachmentCategory === 'proof';
102
+ const endpoint = isProof
103
+ ? `/sheets/${sheetId}/rows/${rowIdNum}/proofs`
104
+ : `/sheets/${sheetId}/rows/${rowIdNum}/attachments`;
105
+ await SteyiGenericFunction_1.smartsheetFileUpload.call(this, endpoint, fileName, binaryData, mimeType, itemIndex);
106
+ }
107
+ else {
108
+ throw new Error('File attachment requires binary data. Please specify a binary property name (e.g., "data") in the URL field.');
109
+ }
110
+ }
111
+ else {
112
+ // For LINK type, use JSON API
113
+ // Ensure rowId is a number
114
+ const rowIdNum = typeof rowId === 'number' ? rowId : parseInt(String(rowId), 10);
115
+ if (isNaN(rowIdNum)) {
116
+ throw new Error(`Invalid row ID: ${rowId}`);
117
+ }
118
+ await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'POST', `/sheets/${sheetId}/rows/${rowIdNum}/attachments`, {
119
+ name: att.name,
120
+ url: att.url,
121
+ attachmentType: 'LINK',
122
+ }, itemIndex);
123
+ }
124
+ }
125
+ async function executeRowOperation(operation, itemIndex) {
126
+ let responseData;
127
+ // Helper function to get sheetId
128
+ const getSheetId = (index) => {
129
+ return this.getNodeParameter('sheetId', index);
130
+ };
131
+ const sheetId = getSheetId(itemIndex);
132
+ switch (operation) {
133
+ case 'addRow': {
134
+ const cellsData = this.getNodeParameter('cells', itemIndex, {});
135
+ const specialOptions = this.getNodeParameter('specialOptions', itemIndex, []);
136
+ let toTop = false;
137
+ let parentId = '';
138
+ if (specialOptions.includes('location')) {
139
+ const location = this.getNodeParameter('location', itemIndex, 'bottom');
140
+ if (location === 'top') {
141
+ toTop = true;
142
+ }
143
+ else if (location === 'parent') {
144
+ parentId = this.getNodeParameter('parentId', itemIndex);
145
+ }
146
+ }
147
+ const discussionsData = specialOptions.includes('discussions')
148
+ ? this.getNodeParameter('discussions', itemIndex, {})
149
+ : { discussion: undefined };
150
+ const attachmentsData = specialOptions.includes('attachments')
151
+ ? this.getNodeParameter('attachments', itemIndex, {})
152
+ : { attachment: undefined };
153
+ // Get column information to determine column types
154
+ let columnsMap = new Map();
155
+ try {
156
+ const columnsResponse = await SteyiSmartsheetApi_1.listColumns.call(this, parseInt(sheetId));
157
+ if (columnsResponse && columnsResponse.data && Array.isArray(columnsResponse.data)) {
158
+ columnsResponse.data.forEach((col) => {
159
+ columnsMap.set(col.id, col);
160
+ });
161
+ }
162
+ }
163
+ catch (error) {
164
+ // If we can't get columns, continue without column type info
165
+ }
166
+ // Helper function to check if a string looks like comma-separated emails
167
+ const looksLikeEmailList = (str) => {
168
+ if (typeof str !== 'string')
169
+ return false;
170
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
171
+ const parts = str.split(',').map(p => p.trim());
172
+ return parts.length > 1 && parts.every(part => emailRegex.test(part));
173
+ };
174
+ const cells = (cellsData.cell || []).map((cell) => {
175
+ let columnId;
176
+ if (cell.columnInputMethod === 'id') {
177
+ columnId = cell.columnIdManual || '';
178
+ }
179
+ else {
180
+ columnId = cell.columnId || '';
181
+ }
182
+ const colIdNum = parseInt(columnId, 10);
183
+ const column = columnsMap.get(colIdNum);
184
+ const columnType = column?.type;
185
+ // Clean up the value - remove surrounding quotes if present
186
+ let cellValue = cell.value;
187
+ if (typeof cellValue === 'string') {
188
+ // Remove surrounding quotes (both single and double)
189
+ cellValue = cellValue.trim();
190
+ if ((cellValue.startsWith('"') && cellValue.endsWith('"')) ||
191
+ (cellValue.startsWith("'") && cellValue.endsWith("'"))) {
192
+ cellValue = cellValue.slice(1, -1);
193
+ }
194
+ }
195
+ // Handle MULTI_CONTACT_LIST and MULTI_PICKLIST columns with objectValue
196
+ // Also handle if value looks like comma-separated emails (fallback for when column type isn't detected)
197
+ if (columnType === 'MULTI_CONTACT_LIST' || columnType === 'MULTI_PICKLIST' ||
198
+ (columnType === undefined && typeof cellValue === 'string' && looksLikeEmailList(cellValue))) {
199
+ // Try to parse the value as JSON if it's a string
200
+ let parsedValue;
201
+ try {
202
+ if (typeof cellValue === 'string') {
203
+ parsedValue = JSON.parse(cellValue);
204
+ }
205
+ else {
206
+ parsedValue = cellValue;
207
+ }
208
+ }
209
+ catch (error) {
210
+ // If parsing fails, treat as regular value (comma-separated string)
211
+ parsedValue = cellValue;
212
+ }
213
+ // If it's already in the correct MULTI_CONTACT format, use it directly
214
+ if (parsedValue && typeof parsedValue === 'object' && parsedValue.objectType === 'MULTI_CONTACT' && parsedValue.values) {
215
+ return {
216
+ columnId: colIdNum,
217
+ objectValue: parsedValue,
218
+ strict: false,
219
+ };
220
+ }
221
+ // If it's an array of contacts (old format), wrap it in MULTI_CONTACT structure
222
+ if (parsedValue && Array.isArray(parsedValue) && parsedValue.length > 0) {
223
+ return {
224
+ columnId: colIdNum,
225
+ objectValue: {
226
+ objectType: 'MULTI_CONTACT',
227
+ values: parsedValue.map((contact) => {
228
+ if (typeof contact === 'string') {
229
+ return { email: contact.trim() };
230
+ }
231
+ if (contact.objectType === 'CONTACT') {
232
+ // Remove objectType from contact objects
233
+ const contactObj = { email: contact.email || contact };
234
+ if (contact.name && contact.name !== contact.email) {
235
+ contactObj.name = contact.name;
236
+ }
237
+ return contactObj;
238
+ }
239
+ return contact;
240
+ }),
241
+ },
242
+ strict: false,
243
+ };
244
+ }
245
+ // Otherwise, format it properly based on column type
246
+ // If columnType is undefined but value looks like emails, treat as MULTI_CONTACT_LIST
247
+ if (columnType === 'MULTI_CONTACT_LIST' ||
248
+ (columnType === undefined && typeof cellValue === 'string' && looksLikeEmailList(cellValue))) {
249
+ // Format for MULTI_CONTACT_LIST
250
+ let contacts = [];
251
+ if (Array.isArray(parsedValue)) {
252
+ contacts = parsedValue;
253
+ }
254
+ else if (typeof parsedValue === 'string') {
255
+ // Try to parse as JSON array first
256
+ try {
257
+ contacts = JSON.parse(parsedValue);
258
+ }
259
+ catch {
260
+ // If JSON parsing fails, check if it's comma-separated
261
+ if (parsedValue.includes(',')) {
262
+ // Split by comma and trim each email
263
+ contacts = parsedValue.split(',').map((email) => email.trim());
264
+ }
265
+ else {
266
+ // If it's a single email, wrap it
267
+ contacts = [parsedValue.trim()];
268
+ }
269
+ }
270
+ }
271
+ // For MULTI_CONTACT_LIST, objectValue should be an object with objectType and values array
272
+ return {
273
+ columnId: colIdNum,
274
+ objectValue: {
275
+ objectType: 'MULTI_CONTACT',
276
+ values: contacts.map((contact) => {
277
+ if (typeof contact === 'string') {
278
+ // For comma-separated emails, return only email field
279
+ return {
280
+ email: contact.trim(),
281
+ };
282
+ }
283
+ // For object format, include email and optionally name
284
+ const contactObj = {
285
+ email: contact.email || contact,
286
+ };
287
+ // Only include name if it's different from email
288
+ if (contact.name && contact.name !== contact.email) {
289
+ contactObj.name = contact.name;
290
+ }
291
+ return contactObj;
292
+ }),
293
+ },
294
+ strict: false,
295
+ };
296
+ }
297
+ else if (columnType === 'MULTI_PICKLIST') {
298
+ // Format for MULTI_PICKLIST
299
+ let options = [];
300
+ if (Array.isArray(parsedValue)) {
301
+ options = parsedValue;
302
+ }
303
+ else if (typeof parsedValue === 'string') {
304
+ try {
305
+ options = JSON.parse(parsedValue);
306
+ }
307
+ catch {
308
+ options = parsedValue.split(',').map((s) => s.trim());
309
+ }
310
+ }
311
+ return {
312
+ columnId: colIdNum,
313
+ objectValue: {
314
+ objectType: 'MULTI_PICKLIST',
315
+ values: options,
316
+ },
317
+ };
318
+ }
319
+ }
320
+ // For regular columns, use value (use cleaned cellValue)
321
+ return {
322
+ columnId: colIdNum,
323
+ value: cellValue,
324
+ };
325
+ });
326
+ const body = {
327
+ cells,
328
+ };
329
+ if (toTop) {
330
+ body.toTop = true;
331
+ }
332
+ if (parentId && parentId !== '') {
333
+ body.parentId = parseInt(parentId, 10);
334
+ }
335
+ // Don't include discussions in the initial row creation body
336
+ // They need to be added separately after row creation
337
+ // Don't include FILE attachments in the initial row creation body
338
+ // They need to be uploaded separately after row creation
339
+ if (attachmentsData.attachment && attachmentsData.attachment.length > 0) {
340
+ const linkAttachments = attachmentsData.attachment.filter(att => att.attachmentType !== 'FILE');
341
+ if (linkAttachments.length > 0) {
342
+ body.attachments = linkAttachments.map((att) => ({
343
+ name: att.name,
344
+ url: att.url,
345
+ attachmentType: 'LINK',
346
+ }));
347
+ }
348
+ }
349
+ // Check if any cells use objectValue (complex column types) - if so, add level=2 parameter
350
+ const hasComplexColumns = body.cells && body.cells.some((cell) => cell.objectValue);
351
+ const endpoint = hasComplexColumns
352
+ ? `/sheets/${sheetId}/rows?level=2`
353
+ : `/sheets/${sheetId}/rows`;
354
+ try {
355
+ responseData = await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'POST', endpoint, body, itemIndex);
356
+ }
357
+ catch (error) {
358
+ // Include request body in error for debugging
359
+ const errorMessage = error.response?.data
360
+ ? `Smartsheet API Error: ${JSON.stringify(error.response.data)}. Request body: ${JSON.stringify(body)}`
361
+ : error.message;
362
+ throw new Error(errorMessage);
363
+ }
364
+ // Add discussion/comment/attachments separately if needed
365
+ // Smartsheet API returns: { result: { id: ... } } or { result: [{ id: ... }] }
366
+ let rowId;
367
+ if (responseData.result) {
368
+ if (Array.isArray(responseData.result)) {
369
+ rowId = responseData.result[0]?.id;
370
+ }
371
+ else {
372
+ rowId = responseData.result.id;
373
+ }
374
+ }
375
+ else if (responseData.id) {
376
+ rowId = responseData.id;
377
+ }
378
+ else if (Array.isArray(responseData) && responseData[0]?.id) {
379
+ rowId = responseData[0].id;
380
+ }
381
+ if (!rowId) {
382
+ throw new Error('Failed to get row ID from response. Response: ' + JSON.stringify(responseData));
383
+ }
384
+ // Handle discussions separately (after row creation)
385
+ if (discussionsData.discussion && discussionsData.discussion.length > 0) {
386
+ for (const disc of discussionsData.discussion) {
387
+ try {
388
+ if (disc.action === 'addToExisting') {
389
+ // Use discussionId from dropdown or manual entry
390
+ const discussionId = disc.discussionId || disc.discussionIdManual;
391
+ if (!discussionId) {
392
+ throw new Error('Discussion ID is required when adding to existing discussion');
393
+ }
394
+ // Add comment to existing discussion
395
+ await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'POST', `/sheets/${sheetId}/discussions/${discussionId}/comments`, {
396
+ text: disc.commentText,
397
+ }, itemIndex);
398
+ }
399
+ else if (disc.action === 'createNew') {
400
+ // Create new discussion
401
+ await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'POST', `/sheets/${sheetId}/rows/${rowId}/discussions`, {
402
+ comment: {
403
+ text: disc.commentText,
404
+ },
405
+ }, itemIndex);
406
+ }
407
+ }
408
+ catch (error) {
409
+ throw new Error(`Error adding discussion: ${error.message}`);
410
+ }
411
+ }
412
+ }
413
+ // Handle attachments separately (after row creation)
414
+ if (attachmentsData.attachment && attachmentsData.attachment.length > 0) {
415
+ for (const att of attachmentsData.attachment) {
416
+ try {
417
+ await handleAttachment.call(this, att, sheetId, rowId, itemIndex);
418
+ }
419
+ catch (error) {
420
+ throw new Error(`Error adding attachment "${att.name}": ${error.message}`);
421
+ }
422
+ }
423
+ }
424
+ break;
425
+ }
426
+ case 'getRow': {
427
+ const rowId = this.getNodeParameter('rowId', itemIndex);
428
+ const include = this.getNodeParameter('include', itemIndex, []);
429
+ const exclude = this.getNodeParameter('exclude', itemIndex, []);
430
+ const queryParams = [];
431
+ if (include.length > 0) {
432
+ queryParams.push(`include=${include.join(',')}`);
433
+ }
434
+ if (exclude.length > 0) {
435
+ queryParams.push(`exclude=${exclude.join(',')}`);
436
+ }
437
+ const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
438
+ responseData = await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'GET', `/sheets/${sheetId}/rows/${rowId}${queryString}`, {}, itemIndex);
439
+ break;
440
+ }
441
+ case 'getRowMapped': {
442
+ const rowId = this.getNodeParameter('rowId', itemIndex);
443
+ const include = this.getNodeParameter('include', itemIndex, []);
444
+ const exclude = this.getNodeParameter('exclude', itemIndex, []);
445
+ const mappingType = this.getNodeParameter('mappingType', itemIndex, 'columnTitle');
446
+ const queryParams = [];
447
+ if (include.length > 0) {
448
+ queryParams.push(`include=${include.join(',')}`);
449
+ }
450
+ if (exclude.length > 0) {
451
+ queryParams.push(`exclude=${exclude.join(',')}`);
452
+ }
453
+ const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
454
+ // Get the row
455
+ const rowResponse = await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'GET', `/sheets/${sheetId}/rows/${rowId}${queryString}`, {}, itemIndex);
456
+ // Get columns for the sheet (needed for both columnTitle and columnId mapping to include title)
457
+ const columnsResponse = await SteyiSmartsheetApi_1.listColumns.call(this, parseInt(sheetId, 10));
458
+ // Helper function to extract columns from various response shapes
459
+ function getColumns(itemJson) {
460
+ if (Array.isArray(itemJson.columns))
461
+ return itemJson.columns;
462
+ if (Array.isArray(itemJson.data) && itemJson.data[0]?.title && itemJson.data[0]?.id) {
463
+ return itemJson.data; // columns in data[]
464
+ }
465
+ if (Array.isArray(itemJson.sheet?.columns))
466
+ return itemJson.sheet.columns;
467
+ return [];
468
+ }
469
+ const columns = getColumns(columnsResponse);
470
+ // Helper function to extract row from various response shapes
471
+ function getRow(itemJson) {
472
+ if (itemJson.row?.cells)
473
+ return itemJson.row;
474
+ if (Array.isArray(itemJson.cells))
475
+ return itemJson;
476
+ if (Array.isArray(itemJson.data) && itemJson.data[0]?.cells)
477
+ return itemJson.data[0];
478
+ return null;
479
+ }
480
+ const row = getRow(rowResponse);
481
+ if (!row) {
482
+ throw new Error('No row found in response');
483
+ }
484
+ // Build mapped object based on mapping type
485
+ let mapped = {};
486
+ // Build lookup: columnId -> column meta (needed for both mapping types)
487
+ const colById = {};
488
+ columns.forEach((col) => {
489
+ colById[String(col.id)] = col;
490
+ });
491
+ if (mappingType === 'columnId') {
492
+ // Map by column ID, but include column title
493
+ for (const cell of row.cells || []) {
494
+ const col = colById[String(cell.columnId)];
495
+ const title = col?.title ?? `column_${cell.columnId}`;
496
+ mapped[String(cell.columnId)] = {
497
+ columnId: cell.columnId,
498
+ title,
499
+ value: cell.value ?? null,
500
+ displayValue: cell.displayValue ?? null,
501
+ objectValue: cell.objectValue ?? null,
502
+ };
503
+ }
504
+ }
505
+ else {
506
+ // Map by column title (default)
507
+ for (const cell of row.cells || []) {
508
+ const col = colById[String(cell.columnId)];
509
+ const title = col?.title ?? `column_${cell.columnId}`;
510
+ mapped[title] = {
511
+ columnId: cell.columnId,
512
+ title,
513
+ value: cell.value ?? null,
514
+ displayValue: cell.displayValue ?? null,
515
+ objectValue: cell.objectValue ?? null,
516
+ // keep useful column metadata too
517
+ columnType: col?.type,
518
+ columnIndex: col?.index,
519
+ primary: col?.primary ?? false,
520
+ };
521
+ }
522
+ }
523
+ // Include all fields from the row response, not just mapped fields
524
+ responseData = {
525
+ ...rowResponse, // Include all original response data (attachments, discussions, etc. if included)
526
+ rowId: row.id,
527
+ rowNumber: row.rowNumber,
528
+ sheetId: row.sheetId ?? parseInt(sheetId, 10),
529
+ mappingType,
530
+ mapped, // Add the mapped fields as a convenience
531
+ };
532
+ break;
533
+ }
534
+ case 'updateRow': {
535
+ const rowId = this.getNodeParameter('rowId', itemIndex);
536
+ const specialOptions = this.getNodeParameter('specialOptions', itemIndex, []);
537
+ let parentId = '';
538
+ if (specialOptions.includes('location')) {
539
+ const location = this.getNodeParameter('location', itemIndex, 'bottom');
540
+ if (location === 'parent') {
541
+ parentId = this.getNodeParameter('parentId', itemIndex);
542
+ }
543
+ }
544
+ const cellsData = this.getNodeParameter('cells', itemIndex, {});
545
+ const discussionsData = specialOptions.includes('discussions')
546
+ ? this.getNodeParameter('discussions', itemIndex, {})
547
+ : { discussion: undefined };
548
+ const attachmentsData = specialOptions.includes('attachments')
549
+ ? this.getNodeParameter('attachments', itemIndex, {})
550
+ : { attachment: undefined };
551
+ // First, get discussions for the row to show in response
552
+ let existingDiscussions = [];
553
+ try {
554
+ const discussionsResponse = await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'GET', `/sheets/${sheetId}/rows/${rowId}/discussions?include=comments`, {}, itemIndex);
555
+ if (discussionsResponse.data && Array.isArray(discussionsResponse.data)) {
556
+ existingDiscussions = discussionsResponse.data;
557
+ }
558
+ else if (Array.isArray(discussionsResponse)) {
559
+ existingDiscussions = discussionsResponse;
560
+ }
561
+ }
562
+ catch (error) {
563
+ // If we can't get discussions, continue without existing discussions
564
+ }
565
+ // Get column information to determine column types
566
+ let columnsMap = new Map();
567
+ try {
568
+ const columnsResponse = await SteyiSmartsheetApi_1.listColumns.call(this, parseInt(sheetId));
569
+ if (columnsResponse && columnsResponse.data && Array.isArray(columnsResponse.data)) {
570
+ columnsResponse.data.forEach((col) => {
571
+ columnsMap.set(col.id, col);
572
+ });
573
+ }
574
+ }
575
+ catch (error) {
576
+ // If we can't get columns, continue without column type info
577
+ }
578
+ // Helper function to check if a string looks like comma-separated emails
579
+ const looksLikeEmailList = (str) => {
580
+ if (typeof str !== 'string')
581
+ return false;
582
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
583
+ const parts = str.split(',').map(p => p.trim());
584
+ return parts.length > 1 && parts.every(part => emailRegex.test(part));
585
+ };
586
+ const cells = (cellsData.cell || []).map((cell) => {
587
+ let columnId;
588
+ if (cell.columnInputMethod === 'id') {
589
+ columnId = cell.columnIdManual || '';
590
+ }
591
+ else {
592
+ columnId = cell.columnId || '';
593
+ }
594
+ const colIdNum = parseInt(columnId, 10);
595
+ const column = columnsMap.get(colIdNum);
596
+ const columnType = column?.type;
597
+ // Clean up the value - remove surrounding quotes if present
598
+ let cellValue = cell.value;
599
+ if (typeof cellValue === 'string') {
600
+ // Remove surrounding quotes (both single and double)
601
+ cellValue = cellValue.trim();
602
+ if ((cellValue.startsWith('"') && cellValue.endsWith('"')) ||
603
+ (cellValue.startsWith("'") && cellValue.endsWith("'"))) {
604
+ cellValue = cellValue.slice(1, -1);
605
+ }
606
+ }
607
+ // Handle MULTI_CONTACT_LIST and MULTI_PICKLIST columns with objectValue
608
+ // Also handle if value looks like comma-separated emails (fallback for when column type isn't detected)
609
+ if (columnType === 'MULTI_CONTACT_LIST' || columnType === 'MULTI_PICKLIST' ||
610
+ (columnType === undefined && typeof cellValue === 'string' && looksLikeEmailList(cellValue))) {
611
+ // Try to parse the value as JSON if it's a string
612
+ let parsedValue;
613
+ try {
614
+ if (typeof cellValue === 'string') {
615
+ parsedValue = JSON.parse(cellValue);
616
+ }
617
+ else {
618
+ parsedValue = cellValue;
619
+ }
620
+ }
621
+ catch (error) {
622
+ // If parsing fails, treat as regular value (comma-separated string)
623
+ parsedValue = cellValue;
624
+ }
625
+ // If it's already in the correct MULTI_CONTACT format, use it directly
626
+ if (parsedValue && typeof parsedValue === 'object' && parsedValue.objectType === 'MULTI_CONTACT' && parsedValue.values) {
627
+ return {
628
+ columnId: colIdNum,
629
+ objectValue: parsedValue,
630
+ strict: false,
631
+ };
632
+ }
633
+ // If it's an array of contacts (old format), wrap it in MULTI_CONTACT structure
634
+ if (parsedValue && Array.isArray(parsedValue) && parsedValue.length > 0) {
635
+ return {
636
+ columnId: colIdNum,
637
+ objectValue: {
638
+ objectType: 'MULTI_CONTACT',
639
+ values: parsedValue.map((contact) => {
640
+ if (typeof contact === 'string') {
641
+ return { email: contact.trim() };
642
+ }
643
+ if (contact.objectType === 'CONTACT') {
644
+ // Remove objectType from contact objects
645
+ const contactObj = { email: contact.email || contact };
646
+ if (contact.name && contact.name !== contact.email) {
647
+ contactObj.name = contact.name;
648
+ }
649
+ return contactObj;
650
+ }
651
+ return contact;
652
+ }),
653
+ },
654
+ strict: false,
655
+ };
656
+ }
657
+ // Otherwise, format it properly based on column type
658
+ // If columnType is undefined but value looks like emails, treat as MULTI_CONTACT_LIST
659
+ if (columnType === 'MULTI_CONTACT_LIST' ||
660
+ (columnType === undefined && typeof cellValue === 'string' && looksLikeEmailList(cellValue))) {
661
+ // Format for MULTI_CONTACT_LIST
662
+ let contacts = [];
663
+ if (Array.isArray(parsedValue)) {
664
+ contacts = parsedValue;
665
+ }
666
+ else if (typeof parsedValue === 'string') {
667
+ // Try to parse as JSON array first
668
+ try {
669
+ contacts = JSON.parse(parsedValue);
670
+ }
671
+ catch {
672
+ // If JSON parsing fails, check if it's comma-separated
673
+ if (parsedValue.includes(',')) {
674
+ // Split by comma and trim each email
675
+ contacts = parsedValue.split(',').map((email) => email.trim());
676
+ }
677
+ else {
678
+ // If it's a single email, wrap it
679
+ contacts = [parsedValue.trim()];
680
+ }
681
+ }
682
+ }
683
+ // For MULTI_CONTACT_LIST, objectValue should be an object with objectType and values array
684
+ return {
685
+ columnId: colIdNum,
686
+ objectValue: {
687
+ objectType: 'MULTI_CONTACT',
688
+ values: contacts.map((contact) => {
689
+ if (typeof contact === 'string') {
690
+ // For comma-separated emails, return only email field
691
+ return {
692
+ email: contact.trim(),
693
+ };
694
+ }
695
+ // For object format, include email and optionally name
696
+ const contactObj = {
697
+ email: contact.email || contact,
698
+ };
699
+ // Only include name if it's different from email
700
+ if (contact.name && contact.name !== contact.email) {
701
+ contactObj.name = contact.name;
702
+ }
703
+ return contactObj;
704
+ }),
705
+ },
706
+ strict: false,
707
+ };
708
+ }
709
+ else if (columnType === 'MULTI_PICKLIST') {
710
+ // Format for MULTI_PICKLIST
711
+ let options = [];
712
+ if (Array.isArray(parsedValue)) {
713
+ options = parsedValue;
714
+ }
715
+ else if (typeof parsedValue === 'string') {
716
+ try {
717
+ options = JSON.parse(parsedValue);
718
+ }
719
+ catch {
720
+ options = parsedValue.split(',').map((s) => s.trim());
721
+ }
722
+ }
723
+ return {
724
+ columnId: colIdNum,
725
+ objectValue: {
726
+ objectType: 'MULTI_PICKLIST',
727
+ values: options,
728
+ },
729
+ };
730
+ }
731
+ }
732
+ // For regular columns, use value (use cleaned cellValue)
733
+ return {
734
+ columnId: colIdNum,
735
+ value: cellValue,
736
+ };
737
+ });
738
+ const body = {
739
+ id: parseInt(rowId, 10),
740
+ cells,
741
+ };
742
+ if (parentId && parentId !== '') {
743
+ body.parentId = parseInt(parentId, 10);
744
+ }
745
+ // Don't include FILE attachments in the row update body
746
+ // They need to be uploaded separately
747
+ if (attachmentsData.attachment && attachmentsData.attachment.length > 0) {
748
+ const linkAttachments = attachmentsData.attachment.filter(att => att.attachmentType !== 'FILE');
749
+ if (linkAttachments.length > 0) {
750
+ body.attachments = linkAttachments.map((att) => ({
751
+ name: att.name,
752
+ url: att.url,
753
+ attachmentType: 'LINK',
754
+ }));
755
+ }
756
+ }
757
+ // Check if any cells use objectValue (complex column types) - if so, add level=2 parameter
758
+ const hasComplexColumns = cells.some((cell) => cell.objectValue);
759
+ const endpoint = hasComplexColumns
760
+ ? `/sheets/${sheetId}/rows?level=2`
761
+ : `/sheets/${sheetId}/rows`;
762
+ try {
763
+ responseData = await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'PUT', endpoint, [body], itemIndex);
764
+ }
765
+ catch (error) {
766
+ // Include request body in error for debugging
767
+ const errorMessage = error.response?.data
768
+ ? `Smartsheet API Error: ${JSON.stringify(error.response.data)}. Request body: ${JSON.stringify([body])}`
769
+ : error.message;
770
+ throw new Error(errorMessage);
771
+ }
772
+ // Include existing discussions in response
773
+ if (existingDiscussions.length > 0) {
774
+ responseData.discussions = existingDiscussions;
775
+ }
776
+ // Handle discussions separately (after row update)
777
+ if (discussionsData.discussion && discussionsData.discussion.length > 0) {
778
+ for (const disc of discussionsData.discussion) {
779
+ try {
780
+ if (disc.action === 'addToExisting') {
781
+ // Use discussionId from dropdown or manual entry
782
+ const discussionId = disc.discussionId || disc.discussionIdManual;
783
+ if (!discussionId) {
784
+ throw new Error('Discussion ID is required when adding to existing discussion');
785
+ }
786
+ // Add comment to existing discussion
787
+ await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'POST', `/sheets/${sheetId}/discussions/${discussionId}/comments`, {
788
+ text: disc.commentText,
789
+ }, itemIndex);
790
+ }
791
+ else if (disc.action === 'createNew') {
792
+ // Create new discussion
793
+ await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'POST', `/sheets/${sheetId}/rows/${rowId}/discussions`, {
794
+ comment: {
795
+ text: disc.commentText,
796
+ },
797
+ }, itemIndex);
798
+ }
799
+ }
800
+ catch (error) {
801
+ throw new Error(`Error adding discussion: ${error.message}`);
802
+ }
803
+ }
804
+ // Refresh discussions in response after adding new ones
805
+ try {
806
+ const updatedDiscussions = await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'GET', `/sheets/${sheetId}/rows/${rowId}/discussions?include=comments`, {}, itemIndex);
807
+ if (updatedDiscussions.data && Array.isArray(updatedDiscussions.data)) {
808
+ responseData.discussions = updatedDiscussions.data;
809
+ }
810
+ else if (Array.isArray(updatedDiscussions)) {
811
+ responseData.discussions = updatedDiscussions;
812
+ }
813
+ }
814
+ catch (error) {
815
+ // Continue if we can't refresh discussions
816
+ }
817
+ }
818
+ // Handle attachments separately (after row update)
819
+ if (attachmentsData.attachment && attachmentsData.attachment.length > 0) {
820
+ for (const att of attachmentsData.attachment) {
821
+ try {
822
+ await handleAttachment.call(this, att, sheetId, parseInt(rowId, 10), itemIndex);
823
+ }
824
+ catch (error) {
825
+ throw new Error(`Error adding attachment "${att.name}": ${error.message}`);
826
+ }
827
+ }
828
+ }
829
+ break;
830
+ }
831
+ case 'deleteRow': {
832
+ const rowId = this.getNodeParameter('rowId', itemIndex);
833
+ await SteyiGenericFunction_1.smartsheetApiRequest.call(this, 'DELETE', `/sheets/${sheetId}/rows?ids=${rowId}`, {}, itemIndex);
834
+ responseData = { success: true, message: 'Row deleted successfully' };
835
+ break;
836
+ }
837
+ default:
838
+ throw new Error(`Unknown row operation: ${operation}`);
839
+ }
840
+ return {
841
+ json: responseData,
842
+ pairedItem: { item: itemIndex },
843
+ };
844
+ }
845
+ exports.executeRowOperation = executeRowOperation;