@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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # n8n-nodes-learningsuite
2
2
 
3
- ![n8n](https://img.shields.io/badge/n8n-1.113.0+-brightgreen)
3
+ ![n8n](https://img.shields.io/badge/n8n-2.17.2+-brightgreen)
4
4
 
5
- ![Version](https://img.shields.io/badge/version-0.1.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.2.3-blue)
6
6
 
7
7
  ![License](https://img.shields.io/badge/license-MIT-green)
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
- - **14 resources** fully supported (Member, Course, Group, Bundle, Hub, Module, Community, Custom Fields, Popup, Webhook, Role, User, Team Member, AI, API Call)
24
- - **87 operations** for maximum flexibility
25
- - **Instant trigger (webhook-based)** with 17 event types for real-time automation
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** (13 operations)
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** (1 operation)
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
- #### 🎉 Initial Release
386
+ #### Initial Release
358
387
 
359
388
  - ✅ Full LearningSuite API integration
360
389
 
361
- - ✅ 14 resources with 87 operations
390
+ - ✅ 15 resources with 90 action endpoints
362
391
 
363
- - ✅ Webhook triggers with 17 event types for real-time automation
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**: 1.112.3+ (tested with latest)
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<import("n8n-workflow").INodePropertyOptions[]>;
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<import("n8n-workflow").INodePropertyOptions[]>;
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 expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 or specify an ID using an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 or specify an ID using an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 Index',
155
- name: 'profileIndex',
156
- type: 'number',
157
- default: null,
158
- 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.',
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
- 'getStoreValues',
173
+ 'updateProfileField',
174
+ 'createFileUploadTarget',
168
175
  ],
169
176
  },
170
177
  },
171
178
  },
172
179
  {
173
- displayName: 'Profile ID',
174
- name: 'profileId',
175
- type: 'string',
176
- default: '',
177
- description: 'If specified, the profile with this ID is used. Takes precedence over Profile Index and Profile Name.',
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: ['setFieldValue', 'setMultipleFieldValues', 'getProfileByCard', 'updateProfileField'],
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, null);
329
+ const profileIndex = normalizeProfileIndex(ctx.getNodeParameter('profileIndex', i, ''));
158
330
  const qs = {};
159
- if (profileIndex !== null && Number.isFinite(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
- fieldValue = await (0, shared_1.uploadFilesFromBinaryProperties)(ctx, i, userId, binaryPropertyNames, fieldType, fileNameOverride);
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 profileIndex = ctx.getNodeParameter('profileIndex', i, null);
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 profileIndex = ctx.getNodeParameter('profileIndex', i, null);
217
- const validProfileIndex = profileIndex !== null && Number.isFinite(profileIndex) ? profileIndex : undefined;
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
- fieldValue = await (0, shared_1.uploadFilesFromBinaryProperties)(ctx, i, userId, binaryPropertyNames, lsType);
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, null);
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 !== null && Number.isFinite(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
- fieldValue = await (0, shared_1.uploadFilesFromBinaryProperties)(ctx, i, userId, binaryPropertyNames, fieldType, fileNameOverride);
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
- const profileId = ctx.getNodeParameter('profileId', i, '');
310
- const profileIndex = ctx.getNodeParameter('profileIndex', i, null);
311
- if (profileId) {
312
- body.profileId = profileId;
557
+ let profileSelector = buildProfileSelector(ctx, i);
558
+ if (fileFieldContext) {
559
+ profileSelector = getEffectiveProfileSelector(fileFieldContext, profileSelector);
313
560
  }
314
- else if (profileIndex !== null && Number.isFinite(profileIndex)) {
315
- body.profileIndex = profileIndex;
561
+ else if (!(await cardAllowsMultipleProfiles(ctx, cardId))) {
562
+ profileSelector = {};
316
563
  }
317
- if (body.profileId === undefined && body.profileIndex === undefined) {
318
- const profileName = ctx.getNodeParameter('profileName', i, '');
319
- if (profileName) {
320
- body.profileName = profileName;
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<import("n8n-workflow").INodePropertyOptions[]>;
3
- export declare function customFields_getDefinitions(this: ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
4
- export declare function customFields_getCategories(this: ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
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.fetchOptions.call(this, '/custom-fields/cards', undefined, ['name', 'title'], ['id']);
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
- return common_1.fetchOptions.call(this, '/custom-fields/definitions', filterCardId ? { customFieldCardId: filterCardId } : undefined, ['label', 'name', 'key'], ['key']);
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.fetchOptions.call(this, '/custom-fields/categories', customFieldCardId ? { customFieldCardId } : undefined, ['name', 'title'], ['id']);
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.lsRequest.call(ctx, 'GET', '/custom-fields/cards/expanded');
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;
@@ -15,6 +15,7 @@ export interface LsTypeDefinition {
15
15
  export interface LsFieldDefinition {
16
16
  key: string;
17
17
  name: string;
18
+ customFieldCardId?: string;
18
19
  typeDefinition?: LsTypeDefinition;
19
20
  }
20
21
  export interface LsCard {
@@ -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, isVideo) {
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: { isVideo },
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, isVideo);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rjsebening/n8n-nodes-learningsuite",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "n8n node for LearningSuite API",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",