@rjsebening/n8n-nodes-learningsuite 1.2.3 → 1.3.1
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 +42 -13
- package/dist/nodes/LearningSuite/LearningSuite.node.d.ts +5 -1
- package/dist/nodes/LearningSuite/LearningSuiteTrigger.node.d.ts +5 -1
- package/dist/nodes/LearningSuite/descriptions/customFields.properties.js +138 -17
- package/dist/nodes/LearningSuite/execute/customFields.handlers.d.ts +1 -0
- package/dist/nodes/LearningSuite/execute/customFields.handlers.js +273 -22
- package/dist/nodes/LearningSuite/methods/loadOptions/common.d.ts +1 -0
- package/dist/nodes/LearningSuite/methods/loadOptions/common.js +5 -0
- package/dist/nodes/LearningSuite/methods/loadOptions/customFields.loadOptions.d.ts +8 -4
- package/dist/nodes/LearningSuite/methods/loadOptions/customFields.loadOptions.js +76 -3
- package/dist/nodes/LearningSuite/shared/customFields.helpers.js +1 -4
- package/dist/nodes/LearningSuite/shared/customFields.shared.d.ts +1 -0
- package/dist/nodes/LearningSuite/shared/customFields.upload.d.ts +1 -1
- package/dist/nodes/LearningSuite/shared/customFields.upload.js +10 -7
- package/dist/nodes/LearningSuite/shared/request.d.ts +3 -0
- package/dist/nodes/LearningSuite/shared/request.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# n8n-nodes-learningsuite
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -20,9 +20,9 @@ This community node uses the public LearningSuite API and is not affiliated with
|
|
|
20
20
|
|
|
21
21
|
## 🚀 Features
|
|
22
22
|
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **Instant trigger (webhook-based)** with
|
|
23
|
+
- **15 resources** fully supported (Member, Course, Group, Bundle, Hub, Module, Community, Custom Fields, Popup, Webhook, Role, User, Team Member, AI, API Call)
|
|
24
|
+
- **90 action endpoints** for maximum flexibility
|
|
25
|
+
- **Instant trigger (webhook-based)** with 18 event types for real-time automation
|
|
26
26
|
- **Polling trigger** with 11 event types for scheduled polling
|
|
27
27
|
- **Flexible API call** resource for custom endpoints
|
|
28
28
|
|
|
@@ -64,12 +64,13 @@ This community node uses the public LearningSuite API and is not affiliated with
|
|
|
64
64
|
- Get community areas, get community badges, get community forums, get community posts
|
|
65
65
|
- Create community post comment
|
|
66
66
|
|
|
67
|
-
### 🔧 **Custom Fields** (
|
|
67
|
+
### 🔧 **Custom Fields** (14 operations)
|
|
68
68
|
|
|
69
69
|
- Get cards, get cards (expanded), get categories, get definitions
|
|
70
70
|
- Get field values, get store, get store values
|
|
71
71
|
- Get profile by card, get profiles, get profiles (expanded)
|
|
72
72
|
- Set field value, set multiple field values, update profile field
|
|
73
|
+
- Upload file from URL
|
|
73
74
|
|
|
74
75
|
### 🎯 **Popup** (4 operations)
|
|
75
76
|
|
|
@@ -87,9 +88,9 @@ This community node uses the public LearningSuite API and is not affiliated with
|
|
|
87
88
|
|
|
88
89
|
- Send push notification
|
|
89
90
|
|
|
90
|
-
### 🤖 **AI** (
|
|
91
|
+
### 🤖 **AI** (3 operations)
|
|
91
92
|
|
|
92
|
-
- RAG Chat
|
|
93
|
+
- Get agent actions, get AI agents, RAG Chat
|
|
93
94
|
|
|
94
95
|
### 🛡️ **Role** (1 operation)
|
|
95
96
|
|
|
@@ -106,6 +107,7 @@ The LearningSuite trigger supports the following events:
|
|
|
106
107
|
### ⚡ Instant Trigger Events (Webhook)
|
|
107
108
|
|
|
108
109
|
- ✅ Community Post Commented
|
|
110
|
+
- ✅ Agent Action Executed
|
|
109
111
|
- ✅ Community Post Created
|
|
110
112
|
- ✅ Community Post Moderated
|
|
111
113
|
- ✅ Course Member Added
|
|
@@ -258,6 +260,19 @@ docker.n8n.io/n8nio/n8n
|
|
|
258
260
|
|
|
259
261
|
```
|
|
260
262
|
|
|
263
|
+
### Custom Field File Uploads
|
|
264
|
+
|
|
265
|
+
The Custom Fields resource supports file, image, video, and audio custom fields.
|
|
266
|
+
|
|
267
|
+
- Use **Set Field Value**, **Set Multiple Field Values**, or **Update Profile Field** when the file is available as n8n binary data.
|
|
268
|
+
- Use **Upload File From URL** when LearningSuite should download a public file URL and append the returned file value to the selected custom field.
|
|
269
|
+
- File fields support **File Value Mode**:
|
|
270
|
+
- **Add**: append new file values and fail if the custom field limit would be exceeded
|
|
271
|
+
- **Replace**: replace existing file values with the uploaded file values
|
|
272
|
+
- **Replace if Limit Reached**: append while possible, otherwise replace existing file values
|
|
273
|
+
- The node respects the LearningSuite file limits defined on the custom field, such as `maxFiles`, `maxImages`, `maxVideos`, and `maxAudios`.
|
|
274
|
+
- For custom field cards with multiple profiles, use Profile ID, Profile Index, or Profile Name to target a specific profile. If the card does not allow multiple profiles, profile parameters are ignored and the default profile is used.
|
|
275
|
+
|
|
261
276
|
### ⚡ Instant Webhook Trigger Setup
|
|
262
277
|
|
|
263
278
|
```
|
|
@@ -352,15 +367,29 @@ npm test
|
|
|
352
367
|
|
|
353
368
|
## 📝 Changelog
|
|
354
369
|
|
|
370
|
+
### Version 1.2.3 (current)
|
|
371
|
+
|
|
372
|
+
#### Custom Fields File Uploads
|
|
373
|
+
|
|
374
|
+
- ✅ Upload file from public URL
|
|
375
|
+
|
|
376
|
+
- ✅ Binary uploads for file, image, video, and audio custom fields
|
|
377
|
+
|
|
378
|
+
- ✅ File Value Mode: add, replace, and replace when the field limit is reached
|
|
379
|
+
|
|
380
|
+
- ✅ Respect custom field file limits (`maxFiles`, `maxImages`, `maxVideos`, `maxAudios`)
|
|
381
|
+
|
|
382
|
+
- ✅ Improved handling for custom field profile cards and default profiles
|
|
383
|
+
|
|
355
384
|
### Version 0.1.0 (2025-09-23)
|
|
356
385
|
|
|
357
|
-
####
|
|
386
|
+
#### Initial Release
|
|
358
387
|
|
|
359
388
|
- ✅ Full LearningSuite API integration
|
|
360
389
|
|
|
361
|
-
- ✅
|
|
390
|
+
- ✅ 15 resources with 90 action endpoints
|
|
362
391
|
|
|
363
|
-
- ✅ Webhook triggers with
|
|
392
|
+
- ✅ Webhook triggers with 18 event types for real-time automation
|
|
364
393
|
|
|
365
394
|
- ✅ Polling triggers with 11 event types
|
|
366
395
|
|
|
@@ -376,7 +405,7 @@ npm test
|
|
|
376
405
|
|
|
377
406
|
## 🛠️ Compatibility
|
|
378
407
|
|
|
379
|
-
- **n8n Version**:
|
|
408
|
+
- **n8n Version**: 2.17.2+ (tested with latest)
|
|
380
409
|
|
|
381
410
|
- **Node Version**: 20+
|
|
382
411
|
|
|
@@ -424,4 +453,4 @@ This unofficial community node is not affiliated with, endorsed by, or sponsored
|
|
|
424
453
|
|
|
425
454
|
- All LearningSuite trademarks and logos belong to LearningSuite.
|
|
426
455
|
|
|
427
|
-
- This node merely provides an interface to the public API.
|
|
456
|
+
- This node merely provides an interface to the public API.
|
|
@@ -36,7 +36,11 @@ export declare class LearningSuite implements INodeType {
|
|
|
36
36
|
hub_getTemplates(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
37
37
|
group_getGroups(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
38
38
|
customFields_getCards(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
39
|
-
customFields_getDefinitions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<
|
|
39
|
+
customFields_getDefinitions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<{
|
|
40
|
+
name: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}[]>;
|
|
43
|
+
customFields_getMediaDefinitions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
40
44
|
customFields_getCategories(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
41
45
|
customFields_getFieldType(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<{
|
|
42
46
|
name: string;
|
|
@@ -4,7 +4,11 @@ export declare class LearningSuiteTrigger implements INodeType {
|
|
|
4
4
|
methods: {
|
|
5
5
|
loadOptions: {
|
|
6
6
|
customFields_getCards(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
7
|
-
customFields_getDefinitions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<
|
|
7
|
+
customFields_getDefinitions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<{
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}[]>;
|
|
11
|
+
customFields_getMediaDefinitions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
8
12
|
customFields_getCategories(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
9
13
|
customFields_getFieldType(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<{
|
|
10
14
|
name: string;
|
|
@@ -88,6 +88,12 @@ exports.customFieldsProperties = [
|
|
|
88
88
|
description: 'Update a custom field value within a profile of a custom field card',
|
|
89
89
|
action: 'Update a custom field value within a profile of a custom field card',
|
|
90
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Upload File From URL',
|
|
93
|
+
value: 'createFileUploadTarget',
|
|
94
|
+
description: 'Upload a file from a public URL and append it to a custom field',
|
|
95
|
+
action: 'Upload a file from a public URL and append it to a custom field',
|
|
96
|
+
},
|
|
91
97
|
],
|
|
92
98
|
},
|
|
93
99
|
{
|
|
@@ -99,7 +105,7 @@ exports.customFieldsProperties = [
|
|
|
99
105
|
},
|
|
100
106
|
required: true,
|
|
101
107
|
default: '',
|
|
102
|
-
description: 'The ID of the user whose custom field data should be accessed. Choose from the list, or specify an ID using an
|
|
108
|
+
description: 'The ID of the user whose custom field data should be accessed. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
103
109
|
displayOptions: {
|
|
104
110
|
show: {
|
|
105
111
|
resource: ['customFields'],
|
|
@@ -112,6 +118,7 @@ exports.customFieldsProperties = [
|
|
|
112
118
|
'getStoreValues',
|
|
113
119
|
'setFieldValue',
|
|
114
120
|
'setMultipleFieldValues',
|
|
121
|
+
'createFileUploadTarget',
|
|
115
122
|
'updateProfileField',
|
|
116
123
|
],
|
|
117
124
|
},
|
|
@@ -122,7 +129,7 @@ exports.customFieldsProperties = [
|
|
|
122
129
|
name: 'customFieldCardId',
|
|
123
130
|
type: 'options',
|
|
124
131
|
default: '',
|
|
125
|
-
description: 'Filter results by a specific custom field card. Choose from the list
|
|
132
|
+
description: 'Filter results by a specific custom field card. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
126
133
|
typeOptions: {
|
|
127
134
|
loadOptionsMethod: 'customFields_getCards',
|
|
128
135
|
},
|
|
@@ -139,7 +146,7 @@ exports.customFieldsProperties = [
|
|
|
139
146
|
type: 'options',
|
|
140
147
|
required: true,
|
|
141
148
|
default: '',
|
|
142
|
-
description: 'The custom field card to use. Choose from the list
|
|
149
|
+
description: 'The custom field card to use. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
143
150
|
typeOptions: {
|
|
144
151
|
loadOptionsMethod: 'customFields_getCards',
|
|
145
152
|
},
|
|
@@ -151,34 +158,41 @@ exports.customFieldsProperties = [
|
|
|
151
158
|
},
|
|
152
159
|
},
|
|
153
160
|
{
|
|
154
|
-
displayName: 'Profile
|
|
155
|
-
name: '
|
|
156
|
-
type: '
|
|
157
|
-
default:
|
|
158
|
-
description: 'If specified, the profile with this
|
|
161
|
+
displayName: 'Profile ID',
|
|
162
|
+
name: 'profileId',
|
|
163
|
+
type: 'string',
|
|
164
|
+
default: '',
|
|
165
|
+
description: 'If specified, the profile with this ID is used. Takes precedence over Profile Index and Profile Name.',
|
|
159
166
|
displayOptions: {
|
|
160
167
|
show: {
|
|
161
168
|
resource: ['customFields'],
|
|
162
169
|
operation: [
|
|
163
170
|
'setFieldValue',
|
|
164
|
-
'updateProfileField',
|
|
165
171
|
'setMultipleFieldValues',
|
|
166
172
|
'getProfileByCard',
|
|
167
|
-
'
|
|
173
|
+
'updateProfileField',
|
|
174
|
+
'createFileUploadTarget',
|
|
168
175
|
],
|
|
169
176
|
},
|
|
170
177
|
},
|
|
171
178
|
},
|
|
172
179
|
{
|
|
173
|
-
displayName: 'Profile
|
|
174
|
-
name: '
|
|
175
|
-
type: '
|
|
176
|
-
default:
|
|
177
|
-
description: 'If specified, the profile with this
|
|
180
|
+
displayName: 'Profile Index',
|
|
181
|
+
name: 'profileIndex',
|
|
182
|
+
type: 'number',
|
|
183
|
+
default: null,
|
|
184
|
+
description: 'If specified, the profile with this index is used. Ignored if Profile ID is set. Takes precedence over Profile Name. If not specified or if the index does not exist for a given field key, the value in the default/first profile is returned.',
|
|
178
185
|
displayOptions: {
|
|
179
186
|
show: {
|
|
180
187
|
resource: ['customFields'],
|
|
181
|
-
operation: [
|
|
188
|
+
operation: [
|
|
189
|
+
'setFieldValue',
|
|
190
|
+
'updateProfileField',
|
|
191
|
+
'setMultipleFieldValues',
|
|
192
|
+
'createFileUploadTarget',
|
|
193
|
+
'getProfileByCard',
|
|
194
|
+
'getStoreValues',
|
|
195
|
+
],
|
|
182
196
|
},
|
|
183
197
|
},
|
|
184
198
|
},
|
|
@@ -191,7 +205,7 @@ exports.customFieldsProperties = [
|
|
|
191
205
|
displayOptions: {
|
|
192
206
|
show: {
|
|
193
207
|
resource: ['customFields'],
|
|
194
|
-
operation: ['updateProfileField', 'getProfileByCard'],
|
|
208
|
+
operation: ['updateProfileField', 'getProfileByCard', 'createFileUploadTarget'],
|
|
195
209
|
},
|
|
196
210
|
},
|
|
197
211
|
},
|
|
@@ -213,6 +227,52 @@ exports.customFieldsProperties = [
|
|
|
213
227
|
},
|
|
214
228
|
default: '',
|
|
215
229
|
},
|
|
230
|
+
{
|
|
231
|
+
displayName: 'Field Key Name or ID',
|
|
232
|
+
name: 'customFieldKey',
|
|
233
|
+
type: 'options',
|
|
234
|
+
description: 'The file, image, video, or audio custom field to append to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
235
|
+
required: true,
|
|
236
|
+
typeOptions: {
|
|
237
|
+
loadOptionsMethod: 'customFields_getMediaDefinitions',
|
|
238
|
+
},
|
|
239
|
+
displayOptions: {
|
|
240
|
+
show: {
|
|
241
|
+
resource: ['customFields'],
|
|
242
|
+
operation: ['createFileUploadTarget'],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
default: '',
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
displayName: 'File Name',
|
|
249
|
+
name: 'customFieldFileName',
|
|
250
|
+
type: 'string',
|
|
251
|
+
default: '',
|
|
252
|
+
placeholder: 'e.g. report.pdf',
|
|
253
|
+
description: 'Optional title for videos. Also used for files in the custom field file value returned by LearningSuite.',
|
|
254
|
+
displayOptions: {
|
|
255
|
+
show: {
|
|
256
|
+
resource: ['customFields'],
|
|
257
|
+
operation: ['createFileUploadTarget'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
displayName: 'Public Download URL',
|
|
263
|
+
name: 'publicDownloadUrl',
|
|
264
|
+
type: 'string',
|
|
265
|
+
default: '',
|
|
266
|
+
required: true,
|
|
267
|
+
placeholder: 'https://example.com/file.pdf',
|
|
268
|
+
description: 'Public URL that LearningSuite downloads and uploads before returning the custom field file value',
|
|
269
|
+
displayOptions: {
|
|
270
|
+
show: {
|
|
271
|
+
resource: ['customFields'],
|
|
272
|
+
operation: ['createFileUploadTarget'],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
216
276
|
{
|
|
217
277
|
displayName: 'Field Type Name or ID',
|
|
218
278
|
name: 'fieldType',
|
|
@@ -348,6 +408,67 @@ exports.customFieldsProperties = [
|
|
|
348
408
|
},
|
|
349
409
|
},
|
|
350
410
|
},
|
|
411
|
+
{
|
|
412
|
+
displayName: 'File Value Mode',
|
|
413
|
+
name: 'fileValueMode',
|
|
414
|
+
type: 'options',
|
|
415
|
+
default: 'add',
|
|
416
|
+
description: 'How to handle existing file values in the custom field',
|
|
417
|
+
options: [
|
|
418
|
+
{
|
|
419
|
+
name: 'Add',
|
|
420
|
+
value: 'add',
|
|
421
|
+
description: 'Append uploaded files and fail if the field limit would be exceeded',
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: 'Replace',
|
|
425
|
+
value: 'replace',
|
|
426
|
+
description: 'Replace all existing file values with the uploaded files',
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'Replace if Limit Reached',
|
|
430
|
+
value: 'replaceIfFull',
|
|
431
|
+
description: 'Append uploaded files, but replace existing files if the field limit would be exceeded',
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
displayOptions: {
|
|
435
|
+
show: {
|
|
436
|
+
resource: ['customFields'],
|
|
437
|
+
operation: ['setFieldValue', 'updateProfileField'],
|
|
438
|
+
fieldType: ['files', 'images', 'videos', 'audios'],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
displayName: 'File Value Mode',
|
|
444
|
+
name: 'fileValueMode',
|
|
445
|
+
type: 'options',
|
|
446
|
+
default: 'add',
|
|
447
|
+
description: 'How to handle existing file values in the custom field',
|
|
448
|
+
options: [
|
|
449
|
+
{
|
|
450
|
+
name: 'Add',
|
|
451
|
+
value: 'add',
|
|
452
|
+
description: 'Append uploaded files and fail if the field limit would be exceeded',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: 'Replace',
|
|
456
|
+
value: 'replace',
|
|
457
|
+
description: 'Replace all existing file values with the uploaded files',
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: 'Replace if Limit Reached',
|
|
461
|
+
value: 'replaceIfFull',
|
|
462
|
+
description: 'Append uploaded files, but replace existing files if the field limit would be exceeded',
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
displayOptions: {
|
|
466
|
+
show: {
|
|
467
|
+
resource: ['customFields'],
|
|
468
|
+
operation: ['setMultipleFieldValues', 'createFileUploadTarget'],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
351
472
|
{
|
|
352
473
|
displayName: 'File Name',
|
|
353
474
|
name: 'fieldValueFileName',
|
|
@@ -9,6 +9,7 @@ export declare const customFieldsHandlers: {
|
|
|
9
9
|
getFieldValues: ExecuteHandler;
|
|
10
10
|
setFieldValue: ExecuteHandler;
|
|
11
11
|
setMultipleFieldValues: ExecuteHandler;
|
|
12
|
+
createFileUploadTarget: ExecuteHandler;
|
|
12
13
|
getProfiles: ExecuteHandler;
|
|
13
14
|
getProfilesExpanded: ExecuteHandler;
|
|
14
15
|
getProfileByCard: ExecuteHandler;
|
|
@@ -95,6 +95,178 @@ function normalizeCustomFieldValue(response) {
|
|
|
95
95
|
return { value: response[0] };
|
|
96
96
|
return { value: response };
|
|
97
97
|
}
|
|
98
|
+
function normalizeValuesArray(value) {
|
|
99
|
+
if (value === undefined || value === null) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return Array.isArray(value) ? value : [value];
|
|
103
|
+
}
|
|
104
|
+
function normalizeStoreFileValuesResponse(response) {
|
|
105
|
+
if (!Array.isArray(response)) {
|
|
106
|
+
return normalizeValuesArray(response);
|
|
107
|
+
}
|
|
108
|
+
if (response.length === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
if (response.length === 1 && Array.isArray(response[0])) {
|
|
112
|
+
return response[0];
|
|
113
|
+
}
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
function normalizeFileValuesResponse(response) {
|
|
117
|
+
return normalizeStoreFileValuesResponse(response);
|
|
118
|
+
}
|
|
119
|
+
function normalizeProfileIndex(value) {
|
|
120
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
124
|
+
const parsed = Number(value);
|
|
125
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
function getMediaFieldLimit(type, typeDefinition) {
|
|
130
|
+
switch (type) {
|
|
131
|
+
case 'files':
|
|
132
|
+
return typeDefinition.maxFiles;
|
|
133
|
+
case 'images':
|
|
134
|
+
return typeDefinition.maxImages;
|
|
135
|
+
case 'videos':
|
|
136
|
+
return typeDefinition.maxVideos;
|
|
137
|
+
case 'audios':
|
|
138
|
+
return typeDefinition.maxAudios;
|
|
139
|
+
default:
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function buildProfileSelector(ctx, i) {
|
|
144
|
+
const profileId = ctx.getNodeParameter('profileId', i, '');
|
|
145
|
+
const profileIndex = normalizeProfileIndex(ctx.getNodeParameter('profileIndex', i, ''));
|
|
146
|
+
const profileName = ctx.getNodeParameter('profileName', i, '');
|
|
147
|
+
const selector = {};
|
|
148
|
+
if (profileId) {
|
|
149
|
+
selector.profileId = profileId;
|
|
150
|
+
}
|
|
151
|
+
else if (profileIndex !== undefined) {
|
|
152
|
+
selector.profileIndex = profileIndex;
|
|
153
|
+
}
|
|
154
|
+
else if (profileName) {
|
|
155
|
+
selector.profileName = profileName;
|
|
156
|
+
}
|
|
157
|
+
return selector;
|
|
158
|
+
}
|
|
159
|
+
function hasProfileSelector(selector) {
|
|
160
|
+
return selector.profileId !== undefined || selector.profileIndex !== undefined || selector.profileName !== undefined;
|
|
161
|
+
}
|
|
162
|
+
function getEffectiveProfileSelector(fieldContext, selector) {
|
|
163
|
+
return fieldContext.multipleProfilesAllowed ? selector : {};
|
|
164
|
+
}
|
|
165
|
+
async function cardAllowsMultipleProfiles(ctx, cardId) {
|
|
166
|
+
const cards = await shared_1.lsRequest.call(ctx, 'GET', '/custom-fields/cards');
|
|
167
|
+
if (!Array.isArray(cards)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const card = cards.find((entry) => {
|
|
171
|
+
if (typeof entry !== 'object' || entry === null)
|
|
172
|
+
return false;
|
|
173
|
+
return entry.id === cardId;
|
|
174
|
+
});
|
|
175
|
+
if (!card || typeof card !== 'object') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return card.multipleProfilesAllowed === true;
|
|
179
|
+
}
|
|
180
|
+
function getFileValueMode(ctx, i) {
|
|
181
|
+
return ctx.getNodeParameter('fileValueMode', i, 'add');
|
|
182
|
+
}
|
|
183
|
+
function countBinaryPropertyNames(binaryPropertyNames) {
|
|
184
|
+
return binaryPropertyNames
|
|
185
|
+
.split(',')
|
|
186
|
+
.map((name) => name.trim())
|
|
187
|
+
.filter((name) => name.length > 0).length;
|
|
188
|
+
}
|
|
189
|
+
function findFileFieldContext(ctx, cards, customFieldKey) {
|
|
190
|
+
var _a, _b, _c;
|
|
191
|
+
for (const card of cards) {
|
|
192
|
+
if (!(card === null || card === void 0 ? void 0 : card.id) || !Array.isArray(card.definitions)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const definition = card.definitions.find((def) => (def === null || def === void 0 ? void 0 : def.key) === customFieldKey);
|
|
196
|
+
if (!definition) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const type = String((_b = (_a = definition.typeDefinition) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : '');
|
|
200
|
+
if (!FILE_FIELD_TYPES.has(type)) {
|
|
201
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Custom field "${customFieldKey}" is not a file, image, video, or audio field.`);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
cardId: card.id,
|
|
205
|
+
type,
|
|
206
|
+
maxItems: getMediaFieldLimit(type, (_c = definition.typeDefinition) !== null && _c !== void 0 ? _c : {}),
|
|
207
|
+
multipleProfilesAllowed: card.multipleProfilesAllowed === true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Unknown custom field "${customFieldKey}".`);
|
|
211
|
+
}
|
|
212
|
+
async function getPublicUrlFileFieldContext(ctx, customFieldKey) {
|
|
213
|
+
const cardsResponse = await shared_1.lsRequest.call(ctx, 'GET', '/custom-fields/cards/expanded');
|
|
214
|
+
if (!Array.isArray(cardsResponse)) {
|
|
215
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Failed to load custom field definitions.');
|
|
216
|
+
}
|
|
217
|
+
return findFileFieldContext(ctx, cardsResponse, customFieldKey);
|
|
218
|
+
}
|
|
219
|
+
async function readExistingFileValues(ctx, userId, customFieldKey, fieldContext, profileSelector, forceProfileContext = false) {
|
|
220
|
+
const effectiveProfileSelector = getEffectiveProfileSelector(fieldContext, profileSelector);
|
|
221
|
+
const useProfileContext = forceProfileContext || fieldContext.multipleProfilesAllowed || hasProfileSelector(effectiveProfileSelector);
|
|
222
|
+
if (useProfileContext) {
|
|
223
|
+
const existingProfileValues = await shared_1.lsRequest.call(ctx, 'GET', `/custom-fields/store/${userId}/profiles/by-card/${fieldContext.cardId}`, { qs: effectiveProfileSelector });
|
|
224
|
+
return {
|
|
225
|
+
existingValues: normalizeFileValuesResponse(existingProfileValues === null || existingProfileValues === void 0 ? void 0 : existingProfileValues[customFieldKey]),
|
|
226
|
+
useProfileContext,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const existingFieldValues = await shared_1.lsRequest.call(ctx, 'GET', `/custom-fields/store/${userId}/fields/${customFieldKey}`);
|
|
230
|
+
return {
|
|
231
|
+
existingValues: normalizeStoreFileValuesResponse(existingFieldValues),
|
|
232
|
+
useProfileContext,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function assertFileValueCountWithinLimit(ctx, customFieldKey, valueCount, maxItems) {
|
|
236
|
+
if (maxItems !== undefined && valueCount > maxItems) {
|
|
237
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Cannot set custom field "${customFieldKey}". Maximum of ${maxItems} file values would be exceeded.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function buildNextFileValues(ctx, customFieldKey, existingValues, uploadedValues, maxItems, mode) {
|
|
241
|
+
assertFileValueCountWithinLimit(ctx, customFieldKey, uploadedValues.length, maxItems);
|
|
242
|
+
if (mode === 'replace') {
|
|
243
|
+
return uploadedValues;
|
|
244
|
+
}
|
|
245
|
+
const appendedValues = [...existingValues, ...uploadedValues];
|
|
246
|
+
if (maxItems === undefined || appendedValues.length <= maxItems) {
|
|
247
|
+
return appendedValues;
|
|
248
|
+
}
|
|
249
|
+
if (mode === 'replaceIfFull') {
|
|
250
|
+
return uploadedValues;
|
|
251
|
+
}
|
|
252
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Cannot add file to custom field "${customFieldKey}". Maximum of ${maxItems} would be exceeded.`);
|
|
253
|
+
}
|
|
254
|
+
async function writeFileValues(ctx, userId, customFieldKey, fieldContext, profileSelector, useProfileContext, nextValues) {
|
|
255
|
+
const effectiveProfileSelector = getEffectiveProfileSelector(fieldContext, profileSelector);
|
|
256
|
+
if (useProfileContext) {
|
|
257
|
+
const updateBody = {
|
|
258
|
+
fieldKey: customFieldKey,
|
|
259
|
+
fieldValue: nextValues,
|
|
260
|
+
...effectiveProfileSelector,
|
|
261
|
+
};
|
|
262
|
+
return await shared_1.lsRequest.call(ctx, 'PUT', `/custom-fields/store/${userId}/profiles/by-card/${fieldContext.cardId}`, {
|
|
263
|
+
body: updateBody,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return await shared_1.lsRequest.call(ctx, 'PUT', `/custom-fields/store/${userId}/fields/${customFieldKey}`, {
|
|
267
|
+
body: { fieldValue: nextValues },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
98
270
|
function readTypedFieldValue(ctx, i, fieldType) {
|
|
99
271
|
switch (fieldType) {
|
|
100
272
|
case 'string':
|
|
@@ -154,9 +326,9 @@ const getStore = async (ctx, i) => {
|
|
|
154
326
|
};
|
|
155
327
|
const getStoreValues = async (ctx, i) => {
|
|
156
328
|
const userId = ctx.getNodeParameter('userId', i);
|
|
157
|
-
const profileIndex = ctx.getNodeParameter('profileIndex', i,
|
|
329
|
+
const profileIndex = normalizeProfileIndex(ctx.getNodeParameter('profileIndex', i, ''));
|
|
158
330
|
const qs = {};
|
|
159
|
-
if (profileIndex !==
|
|
331
|
+
if (profileIndex !== undefined) {
|
|
160
332
|
qs.profileIndex = profileIndex;
|
|
161
333
|
}
|
|
162
334
|
return await shared_1.lsRequest.call(ctx, 'GET', `/custom-fields/store/${userId}/values`, { qs });
|
|
@@ -172,10 +344,24 @@ const setFieldValue = async (ctx, i) => {
|
|
|
172
344
|
const fieldKey = ctx.getNodeParameter('fieldKey', i);
|
|
173
345
|
const fieldType = ctx.getNodeParameter('fieldType', i, '');
|
|
174
346
|
let fieldValue;
|
|
347
|
+
let fileFieldContext;
|
|
348
|
+
let fileProfileSelector;
|
|
349
|
+
let useFileProfileContext = false;
|
|
175
350
|
if (FILE_FIELD_TYPES.has(fieldType)) {
|
|
176
351
|
const binaryPropertyNames = readTypedFieldValue(ctx, i, fieldType);
|
|
177
352
|
const fileNameOverride = ctx.getNodeParameter('fieldValueFileName', i, '').trim() || undefined;
|
|
178
|
-
|
|
353
|
+
const fileValueMode = getFileValueMode(ctx, i);
|
|
354
|
+
fileFieldContext = await getPublicUrlFileFieldContext(ctx, fieldKey);
|
|
355
|
+
fileProfileSelector = buildProfileSelector(ctx, i);
|
|
356
|
+
const { existingValues, useProfileContext } = await readExistingFileValues(ctx, userId, fieldKey, fileFieldContext, fileProfileSelector);
|
|
357
|
+
useFileProfileContext = useProfileContext;
|
|
358
|
+
const uploadCount = countBinaryPropertyNames(binaryPropertyNames);
|
|
359
|
+
if (fileValueMode !== 'replaceIfFull') {
|
|
360
|
+
const expectedValues = fileValueMode === 'replace' ? uploadCount : existingValues.length + uploadCount;
|
|
361
|
+
assertFileValueCountWithinLimit(ctx, fieldKey, expectedValues, fileFieldContext.maxItems);
|
|
362
|
+
}
|
|
363
|
+
const uploadedValues = await (0, shared_1.uploadFilesFromBinaryProperties)(ctx, i, userId, fieldKey, binaryPropertyNames, fieldType, fileNameOverride);
|
|
364
|
+
fieldValue = buildNextFileValues(ctx, fieldKey, existingValues, uploadedValues, fileFieldContext.maxItems, fileValueMode);
|
|
179
365
|
}
|
|
180
366
|
else {
|
|
181
367
|
fieldValue = readTypedFieldValue(ctx, i, fieldType);
|
|
@@ -184,8 +370,7 @@ const setFieldValue = async (ctx, i) => {
|
|
|
184
370
|
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `No value provided for custom field "${fieldKey}".`);
|
|
185
371
|
}
|
|
186
372
|
const profileId = ctx.getNodeParameter('profileId', i, '');
|
|
187
|
-
const
|
|
188
|
-
const validProfileIndex = profileIndex !== null && Number.isFinite(profileIndex) ? profileIndex : undefined;
|
|
373
|
+
const validProfileIndex = normalizeProfileIndex(ctx.getNodeParameter('profileIndex', i, ''));
|
|
189
374
|
const body = { fieldValue };
|
|
190
375
|
if (profileId) {
|
|
191
376
|
body.profileId = profileId;
|
|
@@ -193,6 +378,10 @@ const setFieldValue = async (ctx, i) => {
|
|
|
193
378
|
else if (validProfileIndex !== undefined) {
|
|
194
379
|
body.profileIndex = validProfileIndex;
|
|
195
380
|
}
|
|
381
|
+
if (fileFieldContext && fileProfileSelector) {
|
|
382
|
+
const response = await writeFileValues(ctx, userId, fieldKey, fileFieldContext, fileProfileSelector, useFileProfileContext, fieldValue);
|
|
383
|
+
return normalizeCustomFieldValue(response);
|
|
384
|
+
}
|
|
196
385
|
const response = await shared_1.lsRequest.call(ctx, 'PUT', `/custom-fields/store/${userId}/fields/${fieldKey}`, { body });
|
|
197
386
|
return normalizeCustomFieldValue(response);
|
|
198
387
|
};
|
|
@@ -213,8 +402,8 @@ function buildFieldTypeMap(cards) {
|
|
|
213
402
|
const setMultipleFieldValues = async (ctx, i) => {
|
|
214
403
|
const userId = ctx.getNodeParameter('userId', i);
|
|
215
404
|
const profileId = ctx.getNodeParameter('profileId', i, '');
|
|
216
|
-
const
|
|
217
|
-
const
|
|
405
|
+
const validProfileIndex = normalizeProfileIndex(ctx.getNodeParameter('profileIndex', i, ''));
|
|
406
|
+
const fileValueMode = getFileValueMode(ctx, i);
|
|
218
407
|
const mapperData = ctx.getNodeParameter('fieldValues', i);
|
|
219
408
|
const fieldMappings = (mapperData === null || mapperData === void 0 ? void 0 : mapperData.value) || {};
|
|
220
409
|
const cardsResponse = await shared_1.lsRequest.call(ctx, 'GET', '/custom-fields/cards/expanded');
|
|
@@ -222,6 +411,7 @@ const setMultipleFieldValues = async (ctx, i) => {
|
|
|
222
411
|
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Failed to load custom field definitions.');
|
|
223
412
|
}
|
|
224
413
|
const cards = cardsResponse;
|
|
414
|
+
const expandedCards = cardsResponse;
|
|
225
415
|
const fieldTypeMap = buildFieldTypeMap(cards);
|
|
226
416
|
const entries = Object.entries(fieldMappings).filter(([, value]) => value !== undefined);
|
|
227
417
|
const payload = [];
|
|
@@ -233,7 +423,22 @@ const setMultipleFieldValues = async (ctx, i) => {
|
|
|
233
423
|
let fieldValue;
|
|
234
424
|
if (FILE_FIELD_TYPES.has(lsType)) {
|
|
235
425
|
const binaryPropertyNames = typeof value === 'string' ? value : 'data';
|
|
236
|
-
|
|
426
|
+
const fieldContext = findFileFieldContext(ctx, expandedCards, fieldKey);
|
|
427
|
+
const profileSelector = {};
|
|
428
|
+
if (profileId) {
|
|
429
|
+
profileSelector.profileId = profileId;
|
|
430
|
+
}
|
|
431
|
+
else if (validProfileIndex !== undefined) {
|
|
432
|
+
profileSelector.profileIndex = validProfileIndex;
|
|
433
|
+
}
|
|
434
|
+
const { existingValues } = await readExistingFileValues(ctx, userId, fieldKey, fieldContext, profileSelector);
|
|
435
|
+
const uploadCount = countBinaryPropertyNames(binaryPropertyNames);
|
|
436
|
+
if (fileValueMode !== 'replaceIfFull') {
|
|
437
|
+
const expectedValues = fileValueMode === 'replace' ? uploadCount : existingValues.length + uploadCount;
|
|
438
|
+
assertFileValueCountWithinLimit(ctx, fieldKey, expectedValues, fieldContext.maxItems);
|
|
439
|
+
}
|
|
440
|
+
const uploadedValues = await (0, shared_1.uploadFilesFromBinaryProperties)(ctx, i, userId, fieldKey, binaryPropertyNames, lsType);
|
|
441
|
+
fieldValue = buildNextFileValues(ctx, fieldKey, existingValues, uploadedValues, fieldContext.maxItems, fileValueMode);
|
|
237
442
|
}
|
|
238
443
|
else {
|
|
239
444
|
fieldValue = normalizeSingleFieldValueOrFail(ctx, value, fieldKey, lsType);
|
|
@@ -254,6 +459,37 @@ const setMultipleFieldValues = async (ctx, i) => {
|
|
|
254
459
|
body: payload,
|
|
255
460
|
});
|
|
256
461
|
};
|
|
462
|
+
const createFileUploadTarget = async (ctx, i) => {
|
|
463
|
+
const userId = ctx.getNodeParameter('userId', i);
|
|
464
|
+
const customFieldKey = (ctx.getNodeParameter('customFieldKey', i, '') || ctx.getNodeParameter('fieldKey', i, '')).trim();
|
|
465
|
+
const fileName = ctx.getNodeParameter('customFieldFileName', i, '').trim();
|
|
466
|
+
const publicDownloadUrl = ctx.getNodeParameter('publicDownloadUrl', i, '').trim();
|
|
467
|
+
const profileSelector = buildProfileSelector(ctx, i);
|
|
468
|
+
const fileValueMode = getFileValueMode(ctx, i);
|
|
469
|
+
if (!customFieldKey) {
|
|
470
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'No custom field key provided.');
|
|
471
|
+
}
|
|
472
|
+
if (!publicDownloadUrl) {
|
|
473
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'No public download URL provided.');
|
|
474
|
+
}
|
|
475
|
+
const fieldContext = await getPublicUrlFileFieldContext(ctx, customFieldKey);
|
|
476
|
+
const { existingValues, useProfileContext } = await readExistingFileValues(ctx, userId, customFieldKey, fieldContext, profileSelector);
|
|
477
|
+
const body = { customFieldKey };
|
|
478
|
+
if (fileName) {
|
|
479
|
+
body.fileName = fileName;
|
|
480
|
+
}
|
|
481
|
+
body.publicDownloadUrl = publicDownloadUrl;
|
|
482
|
+
const createFileResponse = (await shared_1.lsRequest.call(ctx, 'POST', `/custom-fields/store/${userId}/files`, {
|
|
483
|
+
body,
|
|
484
|
+
}));
|
|
485
|
+
const customFieldValue = createFileResponse.customFieldValue;
|
|
486
|
+
if (customFieldValue === undefined || customFieldValue === null) {
|
|
487
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'LearningSuite did not return a custom field value for the uploaded file.');
|
|
488
|
+
}
|
|
489
|
+
const uploadedValues = normalizeValuesArray(customFieldValue);
|
|
490
|
+
const nextValues = buildNextFileValues(ctx, customFieldKey, existingValues, uploadedValues, fieldContext.maxItems, fileValueMode);
|
|
491
|
+
return await writeFileValues(ctx, userId, customFieldKey, fieldContext, profileSelector, useProfileContext, nextValues);
|
|
492
|
+
};
|
|
257
493
|
const getProfiles = async (ctx, i) => {
|
|
258
494
|
const userId = ctx.getNodeParameter('userId', i);
|
|
259
495
|
const customFieldCardId = ctx.getNodeParameter('customFieldCardId', i, undefined);
|
|
@@ -274,13 +510,13 @@ const getProfileByCard = async (ctx, i) => {
|
|
|
274
510
|
const userId = ctx.getNodeParameter('userId', i);
|
|
275
511
|
const cardId = ctx.getNodeParameter('customFieldCardId', i);
|
|
276
512
|
const profileId = ctx.getNodeParameter('profileId', i, '');
|
|
277
|
-
const profileIndex = ctx.getNodeParameter('profileIndex', i,
|
|
513
|
+
const profileIndex = normalizeProfileIndex(ctx.getNodeParameter('profileIndex', i, ''));
|
|
278
514
|
const profileName = ctx.getNodeParameter('profileName', i, '');
|
|
279
515
|
const qs = {};
|
|
280
516
|
if (profileId) {
|
|
281
517
|
qs.profileId = profileId;
|
|
282
518
|
}
|
|
283
|
-
else if (profileIndex !==
|
|
519
|
+
else if (profileIndex !== undefined) {
|
|
284
520
|
qs.profileIndex = profileIndex;
|
|
285
521
|
}
|
|
286
522
|
else if (profileName) {
|
|
@@ -294,10 +530,22 @@ const updateProfileField = async (ctx, i) => {
|
|
|
294
530
|
const fieldKey = ctx.getNodeParameter('fieldKey', i);
|
|
295
531
|
const fieldType = ctx.getNodeParameter('fieldType', i, '');
|
|
296
532
|
let fieldValue;
|
|
533
|
+
let fileFieldContext;
|
|
534
|
+
let fileProfileSelector;
|
|
297
535
|
if (FILE_FIELD_TYPES.has(fieldType)) {
|
|
298
536
|
const binaryPropertyNames = readTypedFieldValue(ctx, i, fieldType);
|
|
299
537
|
const fileNameOverride = ctx.getNodeParameter('fieldValueFileName', i, '').trim() || undefined;
|
|
300
|
-
|
|
538
|
+
const fileValueMode = getFileValueMode(ctx, i);
|
|
539
|
+
fileFieldContext = await getPublicUrlFileFieldContext(ctx, fieldKey);
|
|
540
|
+
fileProfileSelector = getEffectiveProfileSelector(fileFieldContext, buildProfileSelector(ctx, i));
|
|
541
|
+
const { existingValues } = await readExistingFileValues(ctx, userId, fieldKey, fileFieldContext, fileProfileSelector, true);
|
|
542
|
+
const uploadCount = countBinaryPropertyNames(binaryPropertyNames);
|
|
543
|
+
if (fileValueMode !== 'replaceIfFull') {
|
|
544
|
+
const expectedValues = fileValueMode === 'replace' ? uploadCount : existingValues.length + uploadCount;
|
|
545
|
+
assertFileValueCountWithinLimit(ctx, fieldKey, expectedValues, fileFieldContext.maxItems);
|
|
546
|
+
}
|
|
547
|
+
const uploadedValues = await (0, shared_1.uploadFilesFromBinaryProperties)(ctx, i, userId, fieldKey, binaryPropertyNames, fieldType, fileNameOverride);
|
|
548
|
+
fieldValue = buildNextFileValues(ctx, fieldKey, existingValues, uploadedValues, fileFieldContext.maxItems, fileValueMode);
|
|
301
549
|
}
|
|
302
550
|
else {
|
|
303
551
|
fieldValue = readTypedFieldValue(ctx, i, fieldType);
|
|
@@ -306,19 +554,21 @@ const updateProfileField = async (ctx, i) => {
|
|
|
306
554
|
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `No value provided for custom field "${fieldKey}".`);
|
|
307
555
|
}
|
|
308
556
|
const body = { fieldKey, fieldValue };
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
body.profileId = profileId;
|
|
557
|
+
let profileSelector = buildProfileSelector(ctx, i);
|
|
558
|
+
if (fileFieldContext) {
|
|
559
|
+
profileSelector = getEffectiveProfileSelector(fileFieldContext, profileSelector);
|
|
313
560
|
}
|
|
314
|
-
else if (
|
|
315
|
-
|
|
561
|
+
else if (!(await cardAllowsMultipleProfiles(ctx, cardId))) {
|
|
562
|
+
profileSelector = {};
|
|
316
563
|
}
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
564
|
+
if (profileSelector.profileId !== undefined) {
|
|
565
|
+
body.profileId = profileSelector.profileId;
|
|
566
|
+
}
|
|
567
|
+
else if (profileSelector.profileIndex !== undefined) {
|
|
568
|
+
body.profileIndex = profileSelector.profileIndex;
|
|
569
|
+
}
|
|
570
|
+
else if (profileSelector.profileName !== undefined) {
|
|
571
|
+
body.profileName = profileSelector.profileName;
|
|
322
572
|
}
|
|
323
573
|
const response = await shared_1.lsRequest.call(ctx, 'PUT', `/custom-fields/store/${userId}/profiles/by-card/${cardId}`, {
|
|
324
574
|
body,
|
|
@@ -335,6 +585,7 @@ exports.customFieldsHandlers = {
|
|
|
335
585
|
getFieldValues,
|
|
336
586
|
setFieldValue,
|
|
337
587
|
setMultipleFieldValues,
|
|
588
|
+
createFileUploadTarget,
|
|
338
589
|
getProfiles,
|
|
339
590
|
getProfilesExpanded,
|
|
340
591
|
getProfileByCard,
|
|
@@ -4,4 +4,5 @@ type LoadOptionRow = IDataObject;
|
|
|
4
4
|
export declare function toOptions(rows: LoadOptionRow[], labelKeys?: string[], valueKeys?: string[]): INodePropertyOptions[];
|
|
5
5
|
export declare function ensureArray<T>(res: T | T[]): T[];
|
|
6
6
|
export declare function fetchOptions(this: ILoadOptionsFunctions, endpoint: string, qs?: IDataObject, labelKeys?: string[], valueKeys?: string[]): Promise<INodePropertyOptions[]>;
|
|
7
|
+
export declare function fetchOptionsAll(this: ILoadOptionsFunctions, endpoint: string, qs?: IDataObject, labelKeys?: string[], valueKeys?: string[]): Promise<INodePropertyOptions[]>;
|
|
7
8
|
export {};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.toOptions = toOptions;
|
|
4
4
|
exports.ensureArray = ensureArray;
|
|
5
5
|
exports.fetchOptions = fetchOptions;
|
|
6
|
+
exports.fetchOptionsAll = fetchOptionsAll;
|
|
6
7
|
const shared_1 = require("../../shared");
|
|
7
8
|
function toOptions(rows, labelKeys = ['name', 'title', 'email'], valueKeys = ['id', 'sid', 'slug']) {
|
|
8
9
|
return rows.map((r) => {
|
|
@@ -20,3 +21,7 @@ async function fetchOptions(endpoint, qs, labelKeys, valueKeys) {
|
|
|
20
21
|
const rows = ensureArray(res);
|
|
21
22
|
return toOptions(rows, labelKeys, valueKeys);
|
|
22
23
|
}
|
|
24
|
+
async function fetchOptionsAll(endpoint, qs, labelKeys, valueKeys) {
|
|
25
|
+
const rows = await shared_1.lsRequestAll.call(this, endpoint, { qs });
|
|
26
|
+
return toOptions(rows, labelKeys, valueKeys);
|
|
27
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
|
2
|
-
export declare function customFields_getCards(this: ILoadOptionsFunctions): Promise<
|
|
3
|
-
export declare function customFields_getDefinitions(this: ILoadOptionsFunctions): Promise<
|
|
4
|
-
|
|
1
|
+
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
|
2
|
+
export declare function customFields_getCards(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
3
|
+
export declare function customFields_getDefinitions(this: ILoadOptionsFunctions): Promise<{
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}[]>;
|
|
7
|
+
export declare function customFields_getMediaDefinitions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
|
+
export declare function customFields_getCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
5
9
|
export declare function customFields_getFieldType(this: ILoadOptionsFunctions): Promise<{
|
|
6
10
|
name: string;
|
|
7
11
|
value: string;
|
|
@@ -2,23 +2,96 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.customFields_getCards = customFields_getCards;
|
|
4
4
|
exports.customFields_getDefinitions = customFields_getDefinitions;
|
|
5
|
+
exports.customFields_getMediaDefinitions = customFields_getMediaDefinitions;
|
|
5
6
|
exports.customFields_getCategories = customFields_getCategories;
|
|
6
7
|
exports.customFields_getFieldType = customFields_getFieldType;
|
|
7
8
|
exports.customFields_getFieldOptions = customFields_getFieldOptions;
|
|
8
9
|
const common_1 = require("./common");
|
|
10
|
+
const shared_1 = require("../../shared");
|
|
9
11
|
const customFields_helpers_1 = require("../../shared/customFields.helpers");
|
|
12
|
+
const MEDIA_FIELD_TYPES = new Set(['files', 'images', 'videos', 'audios']);
|
|
13
|
+
function getMediaFieldMaxInfo(fieldType, typeDefinition) {
|
|
14
|
+
const maxByType = {
|
|
15
|
+
files: typeDefinition === null || typeDefinition === void 0 ? void 0 : typeDefinition.maxFiles,
|
|
16
|
+
images: typeDefinition === null || typeDefinition === void 0 ? void 0 : typeDefinition.maxImages,
|
|
17
|
+
videos: typeDefinition === null || typeDefinition === void 0 ? void 0 : typeDefinition.maxVideos,
|
|
18
|
+
audios: typeDefinition === null || typeDefinition === void 0 ? void 0 : typeDefinition.maxAudios,
|
|
19
|
+
};
|
|
20
|
+
const max = maxByType[fieldType];
|
|
21
|
+
if (typeof max === 'number' && Number.isFinite(max)) {
|
|
22
|
+
return `max. ${max}`;
|
|
23
|
+
}
|
|
24
|
+
if (typeof max === 'string' && max.trim() !== '') {
|
|
25
|
+
return `max. ${max.trim()}`;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function getMediaFieldDetails(fieldType, typeDefinition, cardName) {
|
|
30
|
+
const typeLabel = fieldType.charAt(0).toUpperCase() + fieldType.slice(1);
|
|
31
|
+
return [typeLabel, getMediaFieldMaxInfo(fieldType, typeDefinition), cardName].filter(Boolean).join(', ');
|
|
32
|
+
}
|
|
10
33
|
async function customFields_getCards() {
|
|
11
|
-
return common_1.
|
|
34
|
+
return common_1.fetchOptionsAll.call(this, '/custom-fields/cards', undefined, ['name', 'title'], ['id']);
|
|
12
35
|
}
|
|
13
36
|
async function customFields_getDefinitions() {
|
|
14
37
|
const cardId = this.getCurrentNodeParameter('cardId');
|
|
15
38
|
const customFieldCardId = this.getCurrentNodeParameter('customFieldCardId');
|
|
16
39
|
const filterCardId = cardId || customFieldCardId;
|
|
17
|
-
|
|
40
|
+
const definitions = await shared_1.lsRequestAll.call(this, '/custom-fields/definitions', {
|
|
41
|
+
qs: filterCardId ? { customFieldCardId: filterCardId } : undefined,
|
|
42
|
+
});
|
|
43
|
+
return definitions
|
|
44
|
+
.filter((definition) => typeof definition === 'object' && definition !== null)
|
|
45
|
+
.map((definition) => {
|
|
46
|
+
var _a, _b, _c, _d;
|
|
47
|
+
const definitionRecord = definition;
|
|
48
|
+
const key = definitionRecord.key;
|
|
49
|
+
const typeDefinition = definitionRecord.typeDefinition;
|
|
50
|
+
const fieldType = String((_a = typeDefinition === null || typeDefinition === void 0 ? void 0 : typeDefinition.type) !== null && _a !== void 0 ? _a : '').toLowerCase();
|
|
51
|
+
const fieldName = String((_d = (_c = (_b = definitionRecord.label) !== null && _b !== void 0 ? _b : definitionRecord.name) !== null && _c !== void 0 ? _c : key) !== null && _d !== void 0 ? _d : 'Unknown');
|
|
52
|
+
const value = String(key !== null && key !== void 0 ? key : fieldName);
|
|
53
|
+
if (!MEDIA_FIELD_TYPES.has(fieldType)) {
|
|
54
|
+
return { name: fieldName, value };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
name: `${fieldName} (${getMediaFieldDetails(fieldType, typeDefinition)})`,
|
|
58
|
+
value,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function customFields_getMediaDefinitions() {
|
|
63
|
+
var _a, _b, _c, _d, _e;
|
|
64
|
+
const cards = await shared_1.lsRequestAll.call(this, '/custom-fields/cards/expanded');
|
|
65
|
+
const options = [];
|
|
66
|
+
for (const card of cards) {
|
|
67
|
+
if (typeof card !== 'object' || card === null)
|
|
68
|
+
continue;
|
|
69
|
+
const cardRecord = card;
|
|
70
|
+
const definitions = cardRecord.definitions;
|
|
71
|
+
if (!Array.isArray(definitions))
|
|
72
|
+
continue;
|
|
73
|
+
const cardName = String((_b = (_a = cardRecord.name) !== null && _a !== void 0 ? _a : cardRecord.title) !== null && _b !== void 0 ? _b : '').trim();
|
|
74
|
+
for (const definition of definitions) {
|
|
75
|
+
if (typeof definition !== 'object' || definition === null)
|
|
76
|
+
continue;
|
|
77
|
+
const definitionRecord = definition;
|
|
78
|
+
const key = definitionRecord.key;
|
|
79
|
+
const typeDefinition = definitionRecord.typeDefinition;
|
|
80
|
+
const fieldType = String((_c = typeDefinition === null || typeDefinition === void 0 ? void 0 : typeDefinition.type) !== null && _c !== void 0 ? _c : '').toLowerCase();
|
|
81
|
+
if (typeof key !== 'string' || !MEDIA_FIELD_TYPES.has(fieldType)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const fieldName = String((_e = (_d = definitionRecord.label) !== null && _d !== void 0 ? _d : definitionRecord.name) !== null && _e !== void 0 ? _e : key);
|
|
85
|
+
const details = getMediaFieldDetails(fieldType, typeDefinition, cardName);
|
|
86
|
+
const name = `${fieldName} (${details})`;
|
|
87
|
+
options.push({ name, value: key });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return options.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
18
91
|
}
|
|
19
92
|
async function customFields_getCategories() {
|
|
20
93
|
const customFieldCardId = this.getCurrentNodeParameter('customFieldCardId');
|
|
21
|
-
return common_1.
|
|
94
|
+
return common_1.fetchOptionsAll.call(this, '/custom-fields/categories', customFieldCardId ? { customFieldCardId } : undefined, ['name', 'title'], ['id']);
|
|
22
95
|
}
|
|
23
96
|
async function customFields_getFieldType() {
|
|
24
97
|
const fieldKey = this.getCurrentNodeParameter('fieldKey');
|
|
@@ -6,10 +6,7 @@ exports.getLsSimpleType = getLsSimpleType;
|
|
|
6
6
|
const request_1 = require("./request");
|
|
7
7
|
const customFields_shared_1 = require("./customFields.shared");
|
|
8
8
|
async function fetchFieldDefinition(ctx, fieldKey) {
|
|
9
|
-
const cards = await request_1.
|
|
10
|
-
if (!Array.isArray(cards)) {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
9
|
+
const cards = await request_1.lsRequestAll.call(ctx, '/custom-fields/cards/expanded');
|
|
13
10
|
for (const card of cards) {
|
|
14
11
|
if (!(0, customFields_shared_1.isLsCard)(card) || !Array.isArray(card.definitions)) {
|
|
15
12
|
continue;
|
|
@@ -9,5 +9,5 @@ interface UploadedFileValue {
|
|
|
9
9
|
* Uploads one or more binary properties and returns the array of file value objects.
|
|
10
10
|
* The binaryPropertyNames parameter can be a comma-separated list (e.g. "data,file1,file2").
|
|
11
11
|
*/
|
|
12
|
-
export declare function uploadFilesFromBinaryProperties(ctx: IExecuteFunctions, itemIndex: number, userId: string, binaryPropertyNames: string, fieldType: string, fileNameOverride?: string): Promise<UploadedFileValue[]>;
|
|
12
|
+
export declare function uploadFilesFromBinaryProperties(ctx: IExecuteFunctions, itemIndex: number, userId: string, customFieldKey: string, binaryPropertyNames: string, fieldType: string, fileNameOverride?: string): Promise<UploadedFileValue[]>;
|
|
13
13
|
export {};
|
|
@@ -10,9 +10,13 @@ function resolveFileType(fieldType) {
|
|
|
10
10
|
* Creates a file slot in the LearningSuite custom-field store for a given user.
|
|
11
11
|
* Returns the fileId and the uploadSpec describing how/where to upload.
|
|
12
12
|
*/
|
|
13
|
-
async function createFileSlot(ctx, userId,
|
|
13
|
+
async function createFileSlot(ctx, userId, customFieldKey, fileName) {
|
|
14
|
+
const body = { customFieldKey };
|
|
15
|
+
if (fileName) {
|
|
16
|
+
body.fileName = fileName;
|
|
17
|
+
}
|
|
14
18
|
const response = (await request_1.lsRequest.call(ctx, 'POST', `/custom-fields/store/${userId}/files`, {
|
|
15
|
-
body
|
|
19
|
+
body,
|
|
16
20
|
}));
|
|
17
21
|
const fileId = response.fileId;
|
|
18
22
|
const uploadSpec = response.uploadSpec;
|
|
@@ -155,14 +159,13 @@ function createUploadedFileValue(fileType, fileId, file) {
|
|
|
155
159
|
* 2. Upload the binary data via HTTP PUT
|
|
156
160
|
* 3. Return the file value object to store on the custom field
|
|
157
161
|
*/
|
|
158
|
-
async function uploadSingleFile(ctx, itemIndex, userId, binaryPropertyName, fieldType, fileNameOverride) {
|
|
162
|
+
async function uploadSingleFile(ctx, itemIndex, userId, customFieldKey, binaryPropertyName, fieldType, fileNameOverride) {
|
|
159
163
|
const binaryData = ctx.helpers.assertBinaryData(itemIndex, binaryPropertyName);
|
|
160
164
|
const buffer = await ctx.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
|
|
161
165
|
const fileType = resolveFileType(fieldType);
|
|
162
|
-
const isVideo = fileType === 'videos';
|
|
163
166
|
const mimeType = binaryData.mimeType || 'application/octet-stream';
|
|
164
167
|
const fileName = fileNameOverride || binaryData.fileName || 'upload';
|
|
165
|
-
const { fileId, uploadSpec } = await createFileSlot(ctx, userId,
|
|
168
|
+
const { fileId, uploadSpec } = await createFileSlot(ctx, userId, customFieldKey, fileName);
|
|
166
169
|
if (uploadSpec.type === 'storage') {
|
|
167
170
|
await uploadViaStorage(ctx, uploadSpec, buffer, mimeType);
|
|
168
171
|
}
|
|
@@ -179,7 +182,7 @@ async function uploadSingleFile(ctx, itemIndex, userId, binaryPropertyName, fiel
|
|
|
179
182
|
* Uploads one or more binary properties and returns the array of file value objects.
|
|
180
183
|
* The binaryPropertyNames parameter can be a comma-separated list (e.g. "data,file1,file2").
|
|
181
184
|
*/
|
|
182
|
-
async function uploadFilesFromBinaryProperties(ctx, itemIndex, userId, binaryPropertyNames, fieldType, fileNameOverride) {
|
|
185
|
+
async function uploadFilesFromBinaryProperties(ctx, itemIndex, userId, customFieldKey, binaryPropertyNames, fieldType, fileNameOverride) {
|
|
183
186
|
const names = binaryPropertyNames
|
|
184
187
|
.split(',')
|
|
185
188
|
.map((n) => n.trim())
|
|
@@ -189,7 +192,7 @@ async function uploadFilesFromBinaryProperties(ctx, itemIndex, userId, binaryPro
|
|
|
189
192
|
}
|
|
190
193
|
const results = [];
|
|
191
194
|
for (const name of names) {
|
|
192
|
-
const value = await uploadSingleFile(ctx, itemIndex, userId, name, fieldType, fileNameOverride);
|
|
195
|
+
const value = await uploadSingleFile(ctx, itemIndex, userId, customFieldKey, name, fieldType, fileNameOverride);
|
|
193
196
|
results.push(value);
|
|
194
197
|
}
|
|
195
198
|
return results;
|
|
@@ -11,3 +11,6 @@ export declare function apiRequest(this: ApiThis, { method, path, qs, body, }: {
|
|
|
11
11
|
qs?: IDataObject;
|
|
12
12
|
body?: IDataObject;
|
|
13
13
|
}): Promise<IDataObject | IDataObject[]>;
|
|
14
|
+
export declare function lsRequestAll(this: ApiThis, endpoint: string, { qs }?: {
|
|
15
|
+
qs?: IDataObject;
|
|
16
|
+
}): Promise<IDataObject[]>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.normalizeEndpoint = normalizeEndpoint;
|
|
4
4
|
exports.lsRequest = lsRequest;
|
|
5
5
|
exports.apiRequest = apiRequest;
|
|
6
|
+
exports.lsRequestAll = lsRequestAll;
|
|
6
7
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
7
8
|
function hasRequestWithAuthentication(value) {
|
|
8
9
|
var _a;
|
|
@@ -51,3 +52,20 @@ async function lsRequest(method, endpoint, { qs, body } = {}) {
|
|
|
51
52
|
async function apiRequest({ method, path, qs, body, }) {
|
|
52
53
|
return requestCore.call(this, { method, endpoint: path, qs, body });
|
|
53
54
|
}
|
|
55
|
+
async function lsRequestAll(endpoint, { qs } = {}) {
|
|
56
|
+
const pageSize = 100;
|
|
57
|
+
const maxPages = 500;
|
|
58
|
+
const all = [];
|
|
59
|
+
let offset = 0;
|
|
60
|
+
for (let page = 0; page < maxPages; page++) {
|
|
61
|
+
const res = await lsRequest.call(this, 'GET', endpoint, {
|
|
62
|
+
qs: { ...(qs !== null && qs !== void 0 ? qs : {}), limit: pageSize, offset },
|
|
63
|
+
});
|
|
64
|
+
const rows = Array.isArray(res) ? res : res ? [res] : [];
|
|
65
|
+
all.push(...rows);
|
|
66
|
+
if (rows.length < pageSize)
|
|
67
|
+
break;
|
|
68
|
+
offset += pageSize;
|
|
69
|
+
}
|
|
70
|
+
return all;
|
|
71
|
+
}
|