@rjsebening/n8n-nodes-learningsuite 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,18 +21,20 @@ The node uses the public LearningSuite API. LearningSuite, its trademarks, and i
21
21
  ## 🚀 Features
22
22
 
23
23
  - **16 resources** fully supported (Member, Course, Group, Bundle, Hub, Module, Community, Calendar Event, Custom Fields, Popup, Webhook, Role, User, Team Member, AI, API Call)
24
- - **100 action endpoints** for maximum flexibility
24
+ - **103 action endpoints** for maximum flexibility
25
25
  - **Instant trigger (webhook-based)** with 19 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
 
29
29
  ## 📋 Supported Operations
30
30
 
31
- ### 👤 **Member** (20 operations)
31
+ ### 👤 **Member** (21 operations)
32
32
 
33
33
  - Get member by email, get member by ID, get many members, create a member, update a member, delete a member, find or create member
34
34
  - Activate deactivate member, add member to courses, remove member from courses, add member to bundles, remove member from bundles
35
- - Get member courses, get member course info, get member bundles
35
+ - Get member courses, get member course info, get member bundles, get member AI credits
36
+ - Get many members filters by creation date (`Created After` / `Created Before`)
37
+ - Add member to bundles also updates existing accesses: end date, remove end date, pause until a date, unlimited pause, resume
36
38
  - Get course progress, get module progress, get lesson progress, set course progress, reset course progress
37
39
 
38
40
  ### 📚 **Course** (9 operations)
@@ -51,9 +53,9 @@ The node uses the public LearningSuite API. LearningSuite, its trademarks, and i
51
53
 
52
54
  - Get bundles, get bundle members
53
55
 
54
- ### 🏠 **Hub** (6 operations)
56
+ ### 🏠 **Hub** (7 operations)
55
57
 
56
- - Get many hubs, get hub templates, get hub template variables, create a hub, add hub access, remove hub access
58
+ - Get many hubs, get hub templates, get hub template variables, create a hub, delete a hub, add hub access, remove hub access
57
59
 
58
60
  ### 📖 **Module** (3 operations)
59
61
 
@@ -65,9 +67,9 @@ The node uses the public LearningSuite API. LearningSuite, its trademarks, and i
65
67
  - Create community post, add comment to post
66
68
  - Assign badges to member, remove badges from member
67
69
 
68
- ### 🗓️ **Calendar Event** (4 operations)
70
+ ### 🗓️ **Calendar Event** (5 operations)
69
71
 
70
- - Get many calendar events, create a calendar event, update a calendar event, delete a calendar event
72
+ - Get many calendar events, create a calendar event, update a calendar event, delete a calendar event, get calendar event analytics
71
73
  - Reminders (push/email), per-phase link stages (before/during/after), access via members/groups/bundles
72
74
  - Update maps fields from incoming items automatically
73
75
 
@@ -294,6 +296,7 @@ The Calendar Event resource works on event **series**, never on single dates —
294
296
  - **Clearing a value**: set the field to the expression `{{ null }}`. The API allows this for Description, Duration, and End Date only.
295
297
  - **Link Stages** replace the single link with before/during/after phases. As soon as any phase is set, the During phase is required.
296
298
  - **Access** (members, groups, bundles) can only be set when creating an event; the update endpoint does not accept it.
299
+ - **Get Analytics** reports per member what they did on one occurrence: `infoOpen`, `beforeLinkClick`, `liveJoin`, `afterLinkClick`. The `Occurrence Date` is the occurrence's original date, even if it was moved. By default the whole target group is returned, members with an empty `actions` list are the no-shows; `Only Members With Actions` returns just the participants. Page through with `Limit`/`Offset` (max. 100) until `members` comes back empty.
297
300
 
298
301
  ### ⚡ Instant Webhook Trigger Setup
299
302
 
@@ -133,6 +133,12 @@ exports.calendarEventProperties = [
133
133
  description: 'Delete a calendar event series including all of its occurrences',
134
134
  action: 'Delete a calendar event',
135
135
  },
136
+ {
137
+ name: 'Get Analytics',
138
+ value: 'getAnalytics',
139
+ description: 'Get how members engaged with one occurrence of a calendar event series (info opened, links clicked, live joined)',
140
+ action: 'Get calendar event analytics',
141
+ },
136
142
  {
137
143
  name: 'Get Many',
138
144
  value: 'getAll',
@@ -170,7 +176,7 @@ exports.calendarEventProperties = [
170
176
  name: 'limit',
171
177
  type: 'number',
172
178
  description: 'Max number of results to return',
173
- displayOptions: show('getAll'),
179
+ displayOptions: show('getAll', 'getAnalytics'),
174
180
  typeOptions: { minValue: 1, maxValue: 100 },
175
181
  default: 50,
176
182
  },
@@ -178,7 +184,7 @@ exports.calendarEventProperties = [
178
184
  displayName: 'Offset',
179
185
  name: 'offset',
180
186
  type: 'number',
181
- displayOptions: show('getAll'),
187
+ displayOptions: show('getAll', 'getAnalytics'),
182
188
  typeOptions: { minValue: 0 },
183
189
  default: 0,
184
190
  description: 'Number of results to skip for pagination',
@@ -188,11 +194,29 @@ exports.calendarEventProperties = [
188
194
  name: 'calendarEventSeriesId',
189
195
  type: 'options',
190
196
  typeOptions: { loadOptionsMethod: 'calendarEvent_getCalendarEvents' },
191
- displayOptions: show('update', 'delete'),
197
+ displayOptions: show('update', 'delete', 'getAnalytics'),
192
198
  default: '',
193
199
  required: true,
194
200
  description: 'ID of the calendar event series. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
195
201
  },
202
+ {
203
+ displayName: 'Occurrence Date',
204
+ name: 'occurrenceDate',
205
+ type: 'string',
206
+ placeholder: '2026-07-20',
207
+ displayOptions: show('getAnalytics'),
208
+ default: '',
209
+ required: true,
210
+ description: "The occurrence to report on (YYYY-MM-DD). Use the occurrence's original date in the event's time zone, even if it was later moved to another day.",
211
+ },
212
+ {
213
+ displayName: 'Only Members With Actions',
214
+ name: 'onlyWithActions',
215
+ type: 'boolean',
216
+ displayOptions: show('getAnalytics'),
217
+ default: false,
218
+ description: 'Whether to return only members with at least one recorded action. Leave off to also get the no-shows (members with an empty actions list).',
219
+ },
196
220
  {
197
221
  displayName: 'Name',
198
222
  name: 'name',
@@ -268,7 +268,7 @@ exports.courseProperties = [
268
268
  name: 'videoUrl',
269
269
  type: 'string',
270
270
  default: '',
271
- description: 'A direct-downloadable video URL to be inserted as the first element on the lesson page. If no explicit thumbnail is set, the first frame of the video will be used as thumbnail.',
271
+ description: 'A direct-downloadable video URL to be inserted as the first element on the lesson page. Public Google Drive or Dropbox share links work as well. The video is imported asynchronously; if no explicit thumbnail is set, the first frame of the video will be used as thumbnail.',
272
272
  },
273
273
  ],
274
274
  },
@@ -140,6 +140,23 @@ exports.customFieldsProperties = [
140
140
  },
141
141
  },
142
142
  },
143
+ {
144
+ displayName: 'Field Key Names or IDs',
145
+ name: 'fieldKeys',
146
+ type: 'multiOptions',
147
+ default: [],
148
+ description: 'Only return the definitions with these keys (max. 50). Leave empty to return all. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
149
+ typeOptions: {
150
+ loadOptionsMethod: 'customFields_getDefinitions',
151
+ loadOptionsDependsOn: ['customFieldCardId'],
152
+ },
153
+ displayOptions: {
154
+ show: {
155
+ resource: ['customFields'],
156
+ operation: ['getDefinitions'],
157
+ },
158
+ },
159
+ },
143
160
  {
144
161
  displayName: 'Card Name or ID',
145
162
  name: 'customFieldCardId',
@@ -11,6 +11,12 @@ exports.hubProperties = [
11
11
  displayOptions: { show: { resource: ['hub'] } },
12
12
  options: [
13
13
  { name: 'Create Hub', value: 'create', description: 'Create a new hub from a template', action: 'Create a hub' },
14
+ {
15
+ name: 'Delete Hub',
16
+ value: 'delete',
17
+ description: 'Permanently delete a hub or hub template',
18
+ action: 'Delete a hub',
19
+ },
14
20
  {
15
21
  name: 'Get Hub Template Variables',
16
22
  value: 'getTemplateVariables',
@@ -42,12 +48,19 @@ exports.hubProperties = [
42
48
  displayName: 'Hub Name or ID',
43
49
  name: 'hubId',
44
50
  type: 'options',
45
- displayOptions: { show: { resource: ['hub'], operation: ['addAccess', 'removeAccess'] } },
51
+ displayOptions: { show: { resource: ['hub'], operation: ['addAccess', 'removeAccess', 'delete'] } },
46
52
  typeOptions: { loadOptionsMethod: 'hub_getHubs' },
47
53
  default: '',
48
54
  required: true,
49
55
  description: 'ID of the hub. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
50
56
  },
57
+ {
58
+ displayName: 'The hub is deleted <b>permanently</b>. Hub templates can be deleted as well - pass the template ID via expression.',
59
+ name: 'deleteNotice',
60
+ type: 'notice',
61
+ displayOptions: { show: { resource: ['hub'], operation: ['delete'] } },
62
+ default: '',
63
+ },
51
64
  {
52
65
  displayName: 'You can grant/remove access via <b>Members</b>, <b>Groups</b>, or <b>Bundles</b> – or in combination. At least one selection is required.',
53
66
  name: 'notice',
@@ -19,7 +19,7 @@ exports.memberProperties = [
19
19
  {
20
20
  name: 'Add Bundles to Member',
21
21
  value: 'addToBundles',
22
- description: 'Grant one or more bundles to a member',
22
+ description: 'Grant bundles to a member or update existing bundle accesses (dates, pause, resume)',
23
23
  action: 'Add member to bundles',
24
24
  },
25
25
  {
@@ -60,6 +60,12 @@ exports.memberProperties = [
60
60
  description: 'List members with optional filters',
61
61
  action: 'Get many members',
62
62
  },
63
+ {
64
+ name: 'Get Member AI Credits',
65
+ value: 'getAiCredits',
66
+ description: 'Get the AI credit limits of a member with current usage',
67
+ action: 'Get member AI credits',
68
+ },
63
69
  {
64
70
  name: 'Get Member Bundles',
65
71
  value: 'getBundles',
@@ -136,6 +142,7 @@ exports.memberProperties = [
136
142
  'addToBundles',
137
143
  'removeFromBundles',
138
144
  'getBundles',
145
+ 'getAiCredits',
139
146
  ],
140
147
  },
141
148
  },
@@ -354,6 +361,20 @@ exports.memberProperties = [
354
361
  placeholder: 'Add Option',
355
362
  displayOptions: { show: { resource: ['member'], operation: ['getAll'] } },
356
363
  options: [
364
+ {
365
+ displayName: 'Created After',
366
+ name: 'createdAtGte',
367
+ type: 'dateTime',
368
+ default: '',
369
+ description: 'Only include members created at or after this date',
370
+ },
371
+ {
372
+ displayName: 'Created Before',
373
+ name: 'createdAtLte',
374
+ type: 'dateTime',
375
+ default: '',
376
+ description: 'Only include members created at or before this date',
377
+ },
357
378
  {
358
379
  displayName: 'Days Not Logged In (GTE)',
359
380
  name: 'daysNotLoggedInGte',
@@ -421,6 +442,13 @@ exports.memberProperties = [
421
442
  required: true,
422
443
  description: 'Select one or more bundles. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
423
444
  },
445
+ {
446
+ displayName: 'If the member already has access to a bundle, that access is <b>updated</b>: only the options you set are changed. Accesses through groups are not affected.',
447
+ name: 'bundleAccessNotice',
448
+ type: 'notice',
449
+ displayOptions: { show: { resource: ['member'], operation: ['addToBundles'] } },
450
+ default: '',
451
+ },
424
452
  {
425
453
  displayName: 'Bundle Names or IDs',
426
454
  name: 'bundles',
@@ -451,21 +479,49 @@ exports.memberProperties = [
451
479
  name: 'accessUntilOverride',
452
480
  type: 'dateTime',
453
481
  default: '',
454
- description: 'Override the date/time when access ends',
482
+ description: 'Override the date/time when access ends. The access duration of the bundle is ignored.',
483
+ },
484
+ {
485
+ displayName: 'Disable Access Notification Email',
486
+ name: 'disableAccessNotificationEmail',
487
+ type: 'boolean',
488
+ default: false,
489
+ description: 'Whether the Access Notification Email option should be disabled',
490
+ },
491
+ {
492
+ displayName: 'Pause Until',
493
+ name: 'pauseUntil',
494
+ type: 'dateTime',
495
+ default: '',
496
+ description: 'Pause the access from today until this date (must be after today, only the date is used). An existing pause is replaced; the access duration is extended by the pause. Cannot be combined with Unlimited Pause or Resume Pause.',
497
+ },
498
+ {
499
+ displayName: 'Remove Access Until Override',
500
+ name: 'removeAccessUntilOverride',
501
+ type: 'boolean',
502
+ default: false,
503
+ description: "Whether to remove an existing end date. Access then follows the bundle's access duration, counted from the access given at date. Ignored if Access Until Override or Unlimited Access is set.",
504
+ },
505
+ {
506
+ displayName: 'Resume Pause',
507
+ name: 'resumePause',
508
+ type: 'boolean',
509
+ default: false,
510
+ description: 'Whether to end the current pause today. Does nothing if the access is not paused. Cannot be combined with Pause Until or Unlimited Pause.',
455
511
  },
456
512
  {
457
513
  displayName: 'Unlimited Access',
458
514
  name: 'unlimitedAccess',
459
515
  type: 'boolean',
460
516
  default: false,
461
- description: 'Whether access never expires (overrides “Access Until Override”)',
517
+ description: 'Whether access never expires (overrides “Access Until Override” and clears an end date already stored on the access)',
462
518
  },
463
519
  {
464
- displayName: 'Disable Access Notification Email',
465
- name: 'disableAccessNotificationEmail',
520
+ displayName: 'Unlimited Pause',
521
+ name: 'unlimitedPause',
466
522
  type: 'boolean',
467
523
  default: false,
468
- description: 'Whether the Access Notification Email option should be disabled',
524
+ description: 'Whether to pause the access from today until it is resumed. An existing pause is replaced. Cannot be combined with Pause Until or Resume Pause.',
469
525
  },
470
526
  ],
471
527
  },
@@ -161,6 +161,7 @@ exports.instantProperties = [
161
161
  description: 'Filter by login type. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
162
162
  options: [
163
163
  { name: 'All', value: '' },
164
+ { name: 'App Email & Password', value: 'app-email-password' },
164
165
  { name: 'App Email Code', value: 'app-email-code' },
165
166
  { name: 'Auto Login', value: 'auto-login' },
166
167
  { name: 'Email & Password', value: 'email-password' },
@@ -218,6 +218,7 @@ exports.webhookProperties = [
218
218
  description: 'Filter by login type. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
219
219
  options: [
220
220
  { name: 'All', value: '' },
221
+ { name: 'App Email & Password', value: 'app-email-password' },
221
222
  { name: 'App Email Code', value: 'app-email-code' },
222
223
  { name: 'Auto Login', value: 'auto-login' },
223
224
  { name: 'Email & Password', value: 'email-password' },
@@ -145,6 +145,8 @@ exports.webhookSampleDataProperties = [
145
145
  type: 'options',
146
146
  default: 'email-password',
147
147
  options: [
148
+ { name: 'App Email & Password', value: 'app-email-password' },
149
+ { name: 'App Email Code', value: 'app-email-code' },
148
150
  { name: 'Auto Login', value: 'auto-login' },
149
151
  { name: 'Email & Password', value: 'email-password' },
150
152
  { name: 'Google', value: 'google' },
@@ -1,6 +1,7 @@
1
1
  import type { ExecuteHandler } from '../exec.types';
2
2
  export declare const calendarEventHandlers: {
3
3
  getAll: ExecuteHandler;
4
+ getAnalytics: ExecuteHandler;
4
5
  create: ExecuteHandler;
5
6
  update: ExecuteHandler;
6
7
  delete: ExecuteHandler;
@@ -227,8 +227,25 @@ const deleteCalendarEvent = async (ctx, i) => {
227
227
  }
228
228
  return await shared_1.lsRequest.call(ctx, 'DELETE', `/calendar-events/${calendarEventSeriesId}`);
229
229
  };
230
+ const getAnalytics = async (ctx, i) => {
231
+ const calendarEventSeriesId = String(ctx.getNodeParameter('calendarEventSeriesId', i, '') || '').trim();
232
+ if (!calendarEventSeriesId) {
233
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide the ID of the calendar event.');
234
+ }
235
+ const occurrenceDate = String(ctx.getNodeParameter('occurrenceDate', i, '') || '').trim();
236
+ assertIsoDate(ctx, 'Occurrence Date', occurrenceDate);
237
+ const qs = {
238
+ occurrenceDate,
239
+ limit: ctx.getNodeParameter('limit', i, 50),
240
+ offset: ctx.getNodeParameter('offset', i, 0),
241
+ };
242
+ if (ctx.getNodeParameter('onlyWithActions', i, false) === true)
243
+ qs.onlyWithActions = true;
244
+ return await shared_1.lsRequest.call(ctx, 'GET', `/calendar-events/${calendarEventSeriesId}/analytics`, { qs });
245
+ };
230
246
  exports.calendarEventHandlers = {
231
247
  getAll,
248
+ getAnalytics,
232
249
  create,
233
250
  update,
234
251
  delete: deleteCalendarEvent,
@@ -306,10 +306,16 @@ const getDefinitions = async (ctx, i) => {
306
306
  const limit = ctx.getNodeParameter('limit', i, 15);
307
307
  const offset = ctx.getNodeParameter('offset', i, 0);
308
308
  const customFieldCardId = ctx.getNodeParameter('customFieldCardId', i, undefined);
309
+ const fieldKeys = (0, shared_1.toIdArray)(ctx.getNodeParameter('fieldKeys', i, []));
310
+ if (fieldKeys.length > 50) {
311
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'At most 50 field keys can be requested at once', { itemIndex: i });
312
+ }
309
313
  const qs = { limit, offset };
310
314
  if (customFieldCardId)
311
315
  qs.customFieldCardId = customFieldCardId;
312
- return await shared_1.lsRequest.call(ctx, 'GET', '/custom-fields/definitions', { qs });
316
+ if (fieldKeys.length)
317
+ qs.fieldKeys = fieldKeys;
318
+ return await shared_1.lsRequest.call(ctx, 'GET', '/custom-fields/definitions', { qs, arrayFormat: 'repeat' });
313
319
  };
314
320
  const getCategories = async (ctx, i) => {
315
321
  const limit = ctx.getNodeParameter('limit', i, 15);
@@ -3,6 +3,7 @@ export declare const hubHandlers: {
3
3
  getAll: ExecuteHandler;
4
4
  getTemplates: ExecuteHandler;
5
5
  create: ExecuteHandler;
6
+ delete: ExecuteHandler;
6
7
  addAccess: ExecuteHandler;
7
8
  removeAccess: ExecuteHandler;
8
9
  getTemplateVariables: ExecuteHandler;
@@ -49,6 +49,10 @@ const removeAccess = async (ctx, i) => {
49
49
  body.bundleIds = bundleIds;
50
50
  return await shared_1.lsRequest.call(ctx, 'DELETE', `/hub/${hubId}/access`, { body });
51
51
  };
52
+ const deleteHub = async (ctx, i) => {
53
+ const hubId = ctx.getNodeParameter('hubId', i);
54
+ return await shared_1.lsRequest.call(ctx, 'DELETE', `/hub/${encodeURIComponent(hubId)}`);
55
+ };
52
56
  const getTemplateVariables = async (ctx, i) => {
53
57
  const templateId = ctx.getNodeParameter('templateId', i);
54
58
  const raw = await shared_1.lsRequest.call(ctx, 'GET', `/hub-template/${templateId}/variables`);
@@ -59,6 +63,7 @@ exports.hubHandlers = {
59
63
  getAll,
60
64
  getTemplates,
61
65
  create,
66
+ delete: deleteHub,
62
67
  addAccess,
63
68
  removeAccess,
64
69
  getTemplateVariables,
@@ -19,5 +19,6 @@ export declare const memberHandlers: {
19
19
  addToBundles: ExecuteHandler;
20
20
  removeFromBundles: ExecuteHandler;
21
21
  getBundles: ExecuteHandler;
22
+ getAiCredits: ExecuteHandler;
22
23
  findOrCreate: ExecuteHandler;
23
24
  };
@@ -1,7 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.memberHandlers = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
4
5
  const shared_1 = require("../shared");
6
+ /** Converts an optional date option to IsoDateTime, failing loudly on unparsable input. */
7
+ function isoDateTimeOption(ctx, i, label, value) {
8
+ if (value === undefined || value === null || value === '')
9
+ return undefined;
10
+ const iso = (0, shared_1.toIsoDateTime)(value);
11
+ if (!iso) {
12
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `"${label}" is not a valid date/time: ${String(value)}`, {
13
+ itemIndex: i,
14
+ });
15
+ }
16
+ return iso;
17
+ }
5
18
  const getByEmail = async (ctx, i) => {
6
19
  const email = ctx.getNodeParameter('email', i);
7
20
  const includeGroups = ctx.getNodeParameter('includeGroups', i);
@@ -36,6 +49,12 @@ const getAll = async (ctx, i) => {
36
49
  qs.days_not_logged_in_gte = days;
37
50
  if (includeNever !== undefined)
38
51
  qs.include_never_logged_in = includeNever;
52
+ const createdAtGte = isoDateTimeOption(ctx, i, 'Created After', additionalOptions.createdAtGte);
53
+ const createdAtLte = isoDateTimeOption(ctx, i, 'Created Before', additionalOptions.createdAtLte);
54
+ if (createdAtGte)
55
+ qs.created_at_gte = createdAtGte;
56
+ if (createdAtLte)
57
+ qs.created_at_lte = createdAtLte;
39
58
  return await shared_1.lsRequest.call(ctx, 'GET', '/members', { qs });
40
59
  };
41
60
  const create = async (ctx, i) => {
@@ -168,15 +187,32 @@ const addToBundles = async (ctx, i) => {
168
187
  const bundles = (0, shared_1.toIdArray)(ctx.getNodeParameter('bundles', i));
169
188
  const add = ctx.getNodeParameter('additionalOptions', i, {});
170
189
  const body = { bundles };
171
- if (add.accessGivenAtOverride) {
172
- body.accessGivenAtOverride = String(add.accessGivenAtOverride);
173
- }
190
+ const accessGivenAtOverride = isoDateTimeOption(ctx, i, 'Access Given At Override', add.accessGivenAtOverride);
191
+ if (accessGivenAtOverride)
192
+ body.accessGivenAtOverride = accessGivenAtOverride;
193
+ const accessUntilOverride = isoDateTimeOption(ctx, i, 'Access Until Override', add.accessUntilOverride);
174
194
  if (add.unlimitedAccess === true) {
175
195
  body.unlimitedAccess = true;
176
196
  }
177
- else if (add.accessUntilOverride) {
178
- body.accessUntilOverride = String(add.accessUntilOverride);
197
+ else if (accessUntilOverride) {
198
+ body.accessUntilOverride = accessUntilOverride;
199
+ }
200
+ else if (add.removeAccessUntilOverride === true) {
201
+ body.accessUntilOverride = null;
179
202
  }
203
+ const pauseUntil = isoDateTimeOption(ctx, i, 'Pause Until', add.pauseUntil);
204
+ const pauseOptions = [pauseUntil, add.unlimitedPause === true, add.resumePause === true].filter(Boolean);
205
+ if (pauseOptions.length > 1) {
206
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Pause Until, Unlimited Pause and Resume Pause cannot be combined', {
207
+ itemIndex: i,
208
+ });
209
+ }
210
+ if (pauseUntil)
211
+ body.pauseUntil = pauseUntil;
212
+ if (add.unlimitedPause === true)
213
+ body.unlimitedPause = true;
214
+ if (add.resumePause === true)
215
+ body.resumePause = true;
180
216
  if (typeof add.disableAccessNotificationEmail === 'boolean') {
181
217
  body.disableAccessNotificationEmail = add.disableAccessNotificationEmail;
182
218
  }
@@ -193,6 +229,10 @@ const getBundles = async (ctx, i) => {
193
229
  const memberId = ctx.getNodeParameter('memberId', i);
194
230
  return await shared_1.lsRequest.call(ctx, 'GET', `/members/${memberId}/bundles`);
195
231
  };
232
+ const getAiCredits = async (ctx, i) => {
233
+ const memberId = ctx.getNodeParameter('memberId', i);
234
+ return await shared_1.lsRequest.call(ctx, 'GET', `/members/${encodeURIComponent(memberId)}/ai-credits`);
235
+ };
196
236
  const findOrCreate = async (ctx, i) => {
197
237
  const email = ctx.getNodeParameter('email', i);
198
238
  try {
@@ -240,5 +280,6 @@ exports.memberHandlers = {
240
280
  addToBundles,
241
281
  removeFromBundles,
242
282
  getBundles,
283
+ getAiCredits,
243
284
  findOrCreate,
244
285
  };
@@ -1 +1,3 @@
1
1
  export declare function toIdArray(input: unknown): string[];
2
+ /** Normalizes a date/time to the API's IsoDateTime format (time zone required). Returns undefined if unparsable. */
3
+ export declare function toIsoDateTime(input: unknown): string | undefined;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toIdArray = toIdArray;
4
+ exports.toIsoDateTime = toIsoDateTime;
4
5
  function toIdArray(input) {
5
6
  if (Array.isArray(input))
6
7
  return input.map((v) => String(v).trim()).filter(Boolean);
@@ -13,3 +14,14 @@ function toIdArray(input) {
13
14
  return [];
14
15
  return [String(input).trim()].filter(Boolean);
15
16
  }
17
+ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
18
+ /** Normalizes a date/time to the API's IsoDateTime format (time zone required). Returns undefined if unparsable. */
19
+ function toIsoDateTime(input) {
20
+ const value = String(input !== null && input !== void 0 ? input : '').trim();
21
+ if (!value)
22
+ return undefined;
23
+ if (ISO_DATE_TIME.test(value))
24
+ return value;
25
+ const date = new Date(value);
26
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
27
+ }
@@ -1,9 +1,10 @@
1
1
  import { type IDataObject, type IHttpRequestOptions, type IExecuteFunctions, type IHookFunctions, type ILoadOptionsFunctions, type IWebhookFunctions, type ITriggerFunctions, type IPollFunctions } from 'n8n-workflow';
2
2
  export type ApiThis = IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions | ITriggerFunctions | IPollFunctions;
3
3
  export declare function normalizeEndpoint(endpoint: string): string;
4
- export declare function lsRequest(this: ApiThis, method: IHttpRequestOptions['method'], endpoint: string, { qs, body }?: {
4
+ export declare function lsRequest(this: ApiThis, method: IHttpRequestOptions['method'], endpoint: string, { qs, body, arrayFormat, }?: {
5
5
  qs?: IDataObject;
6
6
  body?: IDataObject;
7
+ arrayFormat?: IHttpRequestOptions['arrayFormat'];
7
8
  }): Promise<IDataObject | IDataObject[]>;
8
9
  export declare function apiRequest(this: ApiThis, { method, path, qs, body, }: {
9
10
  method: IHttpRequestOptions['method'];
@@ -17,7 +17,7 @@ function normalizeEndpoint(endpoint) {
17
17
  }
18
18
  return endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
19
19
  }
20
- async function requestCore({ method, endpoint, qs, body, }) {
20
+ async function requestCore({ method, endpoint, qs, body, arrayFormat, }) {
21
21
  const url = normalizeEndpoint(endpoint);
22
22
  const creds = (await this.getCredentials('learningSuiteApi'));
23
23
  const baseURL = String(creds.baseUrl || '').trim();
@@ -32,6 +32,8 @@ async function requestCore({ method, endpoint, qs, body, }) {
32
32
  };
33
33
  if (qs && Object.keys(qs).length)
34
34
  options.qs = qs;
35
+ if (arrayFormat)
36
+ options.arrayFormat = arrayFormat;
35
37
  if (body && Object.keys(body).length && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method).toUpperCase())) {
36
38
  options.body = body;
37
39
  }
@@ -41,13 +43,11 @@ async function requestCore({ method, endpoint, qs, body, }) {
41
43
  return await this.helpers.httpRequestWithAuthentication.call(this, 'learningSuiteApi', options);
42
44
  }
43
45
  catch (error) {
44
- throw error instanceof n8n_workflow_1.NodeApiError
45
- ? error
46
- : new n8n_workflow_1.NodeApiError(this.getNode(), error);
46
+ throw error instanceof n8n_workflow_1.NodeApiError ? error : new n8n_workflow_1.NodeApiError(this.getNode(), error);
47
47
  }
48
48
  }
49
- async function lsRequest(method, endpoint, { qs, body } = {}) {
50
- return requestCore.call(this, { method, endpoint, qs, body });
49
+ async function lsRequest(method, endpoint, { qs, body, arrayFormat, } = {}) {
50
+ return requestCore.call(this, { method, endpoint, qs, body, arrayFormat });
51
51
  }
52
52
  async function apiRequest({ method, path, qs, body, }) {
53
53
  return requestCore.call(this, { method, endpoint: path, qs, body });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rjsebening/n8n-nodes-learningsuite",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "n8n node for LearningSuite API",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -54,7 +54,7 @@
54
54
  ]
55
55
  },
56
56
  "devDependencies": {
57
- "@n8n/node-cli": "^0.43.2",
57
+ "@n8n/node-cli": "^0.52.6",
58
58
  "@typescript-eslint/parser": "^8.52.0",
59
59
  "eslint": "^9.39.4",
60
60
  "eslint-plugin-n8n-nodes-base": "^1.16.3",