@rjsebening/n8n-nodes-learningsuite 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/README.md +67 -91
  2. package/dist/nodes/LearningSuite/LearningSuite.node.d.ts +6 -0
  3. package/dist/nodes/LearningSuite/LearningSuite.node.js +10 -6
  4. package/dist/nodes/LearningSuite/LearningSuite.node.json +18 -0
  5. package/dist/nodes/LearningSuite/LearningSuiteTrigger.node.js +3 -3
  6. package/dist/nodes/LearningSuite/descriptions/calendarEvent.properties.d.ts +2 -0
  7. package/dist/nodes/LearningSuite/descriptions/calendarEvent.properties.js +385 -0
  8. package/dist/nodes/LearningSuite/descriptions/member.properties.js +1 -1
  9. package/dist/nodes/LearningSuite/descriptions/module.properties.js +1 -1
  10. package/dist/nodes/LearningSuite/descriptions/resource.selector.js +5 -0
  11. package/dist/nodes/LearningSuite/descriptions/trigger.instant.properties.js +3 -3
  12. package/dist/nodes/LearningSuite/descriptions/webhook.properties.js +2 -2
  13. package/dist/nodes/LearningSuite/exec.types.d.ts +2 -2
  14. package/dist/nodes/LearningSuite/execute/calendarEvent.handlers.d.ts +7 -0
  15. package/dist/nodes/LearningSuite/execute/calendarEvent.handlers.js +235 -0
  16. package/dist/nodes/LearningSuite/execute/registry.js +2 -0
  17. package/dist/nodes/LearningSuite/execute/webhook.handlers.js +1 -1
  18. package/dist/nodes/LearningSuite/methods/loadOptions/calendarEvent.loadOptions.d.ts +5 -0
  19. package/dist/nodes/LearningSuite/methods/loadOptions/calendarEvent.loadOptions.js +15 -0
  20. package/dist/nodes/LearningSuite/methods/loadOptions/teamMember.loadOptions.js +1 -1
  21. package/dist/nodes/LearningSuite/methods/resourceMappers/calendarEvent.resourceMapper.d.ts +3 -0
  22. package/dist/nodes/LearningSuite/methods/resourceMappers/calendarEvent.resourceMapper.js +35 -0
  23. package/dist/nodes/LearningSuitePollingTrigger/LearningSuitePollingTrigger.node.js +2 -2
  24. package/package.json +1 -1
package/README.md CHANGED
@@ -2,27 +2,27 @@
2
2
 
3
3
  ![n8n](https://img.shields.io/badge/n8n-2.17.2+-brightgreen)
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.2.3-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.5.0-blue)
6
6
 
7
7
  ![License](https://img.shields.io/badge/license-MIT-green)
8
8
 
9
- An n8n Community Node for the **LearningSuite API**, which allows you to seamlessly integrate your LearningSuite instance into n8n workflows.
9
+ An n8n node for the **LearningSuite API** that connects your LearningSuite instance to n8n workflows. It is developed and maintained by its original author, Jörg Sebening — and LearningSuite has published this integration.
10
10
 
11
11
  ## What is n8n?
12
12
 
13
13
  n8n is an easy-to-use tool that allows you to automate actions between different web apps, such as LearningSuite. Creating so-called "workflows" between apps automates many of the manual tasks. This can save you or your employees a tremendous amount of time.
14
14
 
15
- ## ⚖️ Legal Notice
15
+ ## ⚖️ Maintainer & Legal Notice
16
16
 
17
- This community node uses the public LearningSuite API and is not affiliated with or sponsored by LearningSuite. All trademarks belong to their respective owners.
17
+ This repository is the original source of the LearningSuite n8n integration, developed and maintained by Jörg Sebening ([@rjsebening](https://github.com/rjsebening)) as its official maintainer. LearningSuite has published this integration.
18
18
 
19
- **Note**: This is a community-developed node for the LearningSuite API. For official support, please contact LearningSuite directly.
19
+ The node uses the public LearningSuite API. LearningSuite, its trademarks, and its logos belong to LearningSuite.
20
20
 
21
21
  ## 🚀 Features
22
22
 
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
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
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
 
@@ -30,75 +30,85 @@ This community node uses the public LearningSuite API and is not affiliated with
30
30
 
31
31
  ### 👤 **Member** (20 operations)
32
32
 
33
- - Get by email, get by ID, get all, create, update, delete, find or create
34
- - Activate/deactivate, add/remove courses, add/remove bundles
35
- - Get courses, get course information, get member bundles
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
+ - 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
36
36
  - Get course progress, get module progress, get lesson progress, set course progress, reset course progress
37
37
 
38
- ### 📚 **Course** (7 operations)
38
+ ### 📚 **Course** (9 operations)
39
39
 
40
- - Get published courses, get course modules, get course modules for member
41
- - Get course members, get course access requests, get course submissions, create lesson
40
+ - Get published courses, publish course, get course modules, get course modules for member
41
+ - Get course members, get course access requests, get course submissions
42
+ - Create lesson, update lesson
42
43
 
43
- ### 👥 **Group** (12 operations)
44
+ ### 👥 **Group** (13 operations)
44
45
 
45
- - Add bundles to group, add courses to group, add members to groups, add members to groups (summary)
46
- - Create group, delete group, find group by name, find or create group
47
- - Get group by ID, get group courses, get many, remove courses from group, remove members from groups
46
+ - Create a group, delete a group, find groups by name, find or create group, get group by ID, get many groups
47
+ - Add members to groups, add members to groups summary, remove members from groups
48
+ - Add courses to group, remove courses from group, get group courses, add bundles to group
48
49
 
49
50
  ### 📦 **Bundle** (2 operations)
50
51
 
51
- - Get all, get bundle members
52
+ - Get bundles, get bundle members
52
53
 
53
54
  ### 🏠 **Hub** (6 operations)
54
55
 
55
- - Get all, get templates, create, give hub access, remove hub access, get hub template variables
56
+ - Get many hubs, get hub templates, get hub template variables, create a hub, add hub access, remove hub access
56
57
 
57
58
  ### 📖 **Module** (3 operations)
58
59
 
59
- - Get module lessons, get module sections, change module access for member
60
+ - Get module lessons, get module sections, change module access
60
61
 
61
- ### 💬 **Community** (7 operations)
62
+ ### 💬 **Community** (8 operations)
62
63
 
63
- - Assign badges to member, remove community badges from member
64
- - Get community areas, get community badges, get community forums, get community posts
65
- - Create community post comment
64
+ - Get community areas, get community forums, get community posts, get community badges
65
+ - Create community post, add comment to post
66
+ - Assign badges to member, remove badges from member
67
+
68
+ ### 🗓️ **Calendar Event** (4 operations)
69
+
70
+ - Get many calendar events, create a calendar event, update a calendar event, delete a calendar event
71
+ - Reminders (push/email), per-phase link stages (before/during/after), access via members/groups/bundles
72
+ - Update maps fields from incoming items automatically
66
73
 
67
74
  ### 🔧 **Custom Fields** (14 operations)
68
75
 
69
- - Get cards, get cards (expanded), get categories, get definitions
70
- - Get field values, get store, get store values
71
- - Get profile by card, get profiles, get profiles (expanded)
72
- - Set field value, set multiple field values, update profile field
73
- - Upload file from URL
76
+ - Retrieve all custom field cards, retrieve custom field cards including their definitions and categories
77
+ - Retrieve custom field categories, retrieve custom field definitions
78
+ - Retrieve the values of a custom field for a user, get all custom field values of a user for a given profile
79
+ - Retrieve custom field profiles of a user, retrieve custom field profiles including their values, retrieve the values of a custom field card profile for a user
80
+ - Retrieve the complete custom field store of a user
81
+ - Set the value of a single custom field for a user, set multiple custom field values for a user in one request, update a custom field value within a profile of a custom field card
82
+ - Upload a file from a public URL and append it to a custom field
74
83
 
75
84
  ### 🎯 **Popup** (4 operations)
76
85
 
77
- - Get all, get popup, trigger popup for member, delete popup trigger
86
+ - Get many popups, get a popup, trigger popup for member, remove popup trigger for member
78
87
 
79
- ### 🔗 **Webhook** (5 operations)
88
+ ### 🔗 **Webhook** (6 operations)
80
89
 
81
- - Get/create/update/delete subscriptions, get sample data
90
+ - Get webhook subscription, get webhook subscriptions, create webhook subscription, update webhook subscription, delete webhook subscription
91
+ - Get webhook sample data
82
92
 
83
93
  ### 👤 **Team Member** (3 operations)
84
94
 
85
- - Get many, get by email, get by ID
95
+ - Get team members, get team member by email, get team member by ID
86
96
 
87
- ### 📢 **User** (1 operation)
97
+ ### 📢 **User** (2 operations)
88
98
 
89
- - Send push notification
99
+ - Send push notification, send login email
90
100
 
91
101
  ### 🤖 **AI** (4 operations)
92
102
 
93
- - Agent chat, concierge chat, get agent actions, get AI agents
103
+ - Send message to AI agent, send message to AI concierge, list AI agents, list agent actions
94
104
 
95
105
  ### 🛡️ **Role** (1 operation)
96
106
 
97
- - Get all
107
+ - Get many roles
98
108
 
99
109
  ### 🛠️ **API Call** (1 operation)
100
110
 
101
- - Make a request (for any API calls)
111
+ - Custom API call (for any endpoint)
102
112
 
103
113
  ## 🎣 Trigger Events
104
114
 
@@ -106,14 +116,15 @@ The LearningSuite trigger supports the following events:
106
116
 
107
117
  ### ⚡ Instant Trigger Events (Webhook)
108
118
 
109
- - ✅ Community Post Commented
110
119
  - ✅ Agent Action Executed
120
+ - ✅ AI Agent Limit for Inaccessible Course Content Exceeded
121
+ - ✅ Community Post Commented
111
122
  - ✅ Community Post Created
112
123
  - ✅ Community Post Moderated
113
124
  - ✅ Course Member Added
114
125
  - ✅ Course Progress Changed
115
126
  - ✅ Course Updated
116
- - ✅ Custom Field Value Changed
127
+ - ✅ Custom Field Value Changed Events
117
128
  - ✅ Custom Popup Interaction
118
129
  - ✅ Exam Completed
119
130
  - ✅ Exam Graded
@@ -273,6 +284,17 @@ The Custom Fields resource supports file, image, video, and audio custom fields.
273
284
  - The node respects the LearningSuite file limits defined on the custom field, such as `maxFiles`, `maxImages`, `maxVideos`, and `maxAudios`.
274
285
  - 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
286
 
287
+ ### Calendar Events
288
+
289
+ The Calendar Event resource works on event **series**, never on single dates — a weekly series is one entry, no matter how often it takes place.
290
+
291
+ - **Get Many** filters by series lifetime: `From Date`/`To Date` select series whose lifetime overlaps the range, widened by one day for timezone tolerance. A series can therefore be returned although none of its dates falls inside the exact range. Calculate the individual dates from `startDate`, `startTime`, `timeZone`, `duration`, `repetitionInterval`, and `endDate`.
292
+ - **Dates and times** use `YYYY-MM-DD` and `HH:mm` and are interpreted in the event's IANA time zone (e.g. `Europe/Berlin`).
293
+ - **Update** maps its fields from the incoming item automatically, so a Get Many result can be piped straight into it. Only mapped fields are sent, everything else keeps its current value.
294
+ - **Clearing a value**: set the field to the expression `{{ null }}`. The API allows this for Description, Duration, and End Date only.
295
+ - **Link Stages** replace the single link with before/during/after phases. As soon as any phase is set, the During phase is required.
296
+ - **Access** (members, groups, bundles) can only be set when creating an event; the update endpoint does not accept it.
297
+
276
298
  ### ⚡ Instant Webhook Trigger Setup
277
299
 
278
300
  ```
@@ -367,41 +389,7 @@ npm test
367
389
 
368
390
  ## 📝 Changelog
369
391
 
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
-
384
- ### Version 0.1.0 (2025-09-23)
385
-
386
- #### Initial Release
387
-
388
- - ✅ Full LearningSuite API integration
389
-
390
- - ✅ 15 resources with 90 action endpoints
391
-
392
- - ✅ Webhook triggers with 18 event types for real-time automation
393
-
394
- - ✅ Polling triggers with 11 event types
395
-
396
- - ✅ "Find-or-Create" logic for members and groups
397
-
398
- - ✅ Custom Fields resource with full CRUD support
399
-
400
- - ✅ Course progress management (get/set/reset)
401
-
402
- - ✅ Flexible parameter configuration
403
-
404
- - ✅ API call resource for custom endpoints
392
+ All notable changes are documented in [CHANGELOG.md](CHANGELOG.md).
405
393
 
406
394
  ## 🛠️ Compatibility
407
395
 
@@ -419,7 +407,9 @@ This project is licensed under the [MIT License].
419
407
 
420
408
  ### Report Issues
421
409
 
422
- For bugs or feature requests, please create a [GitHub Issue](https://github.com/rjsebening/n8n-nodes-learningsuite/issues).
410
+ For bugs or feature requests **in this node**, please create a [GitHub Issue](https://github.com/rjsebening/n8n-nodes-learningsuite/issues).
411
+
412
+ For questions about the LearningSuite API itself or your LearningSuite account, contact LearningSuite support.
423
413
 
424
414
  ### FAQ
425
415
 
@@ -440,17 +430,3 @@ A: The default URL is `https://api.learningsuite.io/api/v1`. For custom domains,
440
430
  ⭐ **Like this node?** Give us a star on GitHub!
441
431
 
442
432
  💡 **Feature request?** Open an issue - we're always open to improvements!
443
-
444
- ## 📋 Disclaimer
445
-
446
- This unofficial community node is not affiliated with, endorsed by, or sponsored by LearningSuite. It exclusively uses the publicly available LearningSuite API in accordance with its Terms of Use.
447
-
448
- **Important Notes:**
449
-
450
- - This node is developed and maintained by the community.
451
-
452
- - For issues with the LearningSuite API, contact official LearningSuite Support.
453
-
454
- - All LearningSuite trademarks and logos belong to LearningSuite.
455
-
456
- - This node merely provides an interface to the public API.
@@ -1,4 +1,5 @@
1
1
  import { type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription } from 'n8n-workflow';
2
+ import { getCalendarEventResourceMapperFields } from './methods/resourceMappers/calendarEvent.resourceMapper';
2
3
  import { getTemplateVariablesResourceMapperFields } from './methods/resourceMappers/hub.resourceMapper';
3
4
  import { getMultipleCustomFieldValueResourceMapperFields } from './methods/resourceMappers/customFields.resourceMapper';
4
5
  export declare class LearningSuite implements INodeType {
@@ -58,11 +59,16 @@ export declare class LearningSuite implements INodeType {
58
59
  community_getForums(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
59
60
  community_getBadges(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
60
61
  community_getLatestPosts(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
62
+ calendarEvent_getCalendarEvents(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<{
63
+ name: string;
64
+ value: string;
65
+ }[]>;
61
66
  bundle_getBundles(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
62
67
  ai_getAgentActions(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
63
68
  ai_getAiAgents(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
64
69
  };
65
70
  resourceMapping: {
71
+ getCalendarEventResourceMapperFields: typeof getCalendarEventResourceMapperFields;
66
72
  getTemplateVariablesResourceMapperFields: typeof getTemplateVariablesResourceMapperFields;
67
73
  getMultipleCustomFieldValueResourceMapperFields: typeof getMultipleCustomFieldValueResourceMapperFields;
68
74
  };
@@ -38,6 +38,7 @@ const n8n_workflow_1 = require("n8n-workflow");
38
38
  // methods - loadOptions
39
39
  const loAi = __importStar(require("./methods/loadOptions/ai.loadOptions"));
40
40
  const loBundle = __importStar(require("./methods/loadOptions/bundle.loadOptions"));
41
+ const loCalendarEvent = __importStar(require("./methods/loadOptions/calendarEvent.loadOptions"));
41
42
  const loCommunity = __importStar(require("./methods/loadOptions/community.loadOptions"));
42
43
  const loCourse = __importStar(require("./methods/loadOptions/course.loadOptions"));
43
44
  const loGroup = __importStar(require("./methods/loadOptions/group.loadOptions"));
@@ -49,12 +50,14 @@ const loRole = __importStar(require("./methods/loadOptions/role.loadOptions"));
49
50
  const loWebhook = __importStar(require("./methods/loadOptions/webhook.loadOptions"));
50
51
  const loTeamMember = __importStar(require("./methods/loadOptions/teamMember.loadOptions"));
51
52
  const loCustomFields = __importStar(require("./methods/loadOptions/customFields.loadOptions"));
53
+ const calendarEvent_resourceMapper_1 = require("./methods/resourceMappers/calendarEvent.resourceMapper");
52
54
  const hub_resourceMapper_1 = require("./methods/resourceMappers/hub.resourceMapper");
53
55
  const customFields_resourceMapper_1 = require("./methods/resourceMappers/customFields.resourceMapper");
54
56
  // properties
55
57
  const resource_selector_1 = require("./descriptions/resource.selector");
56
58
  const apiCall_properties_1 = require("./descriptions/apiCall.properties");
57
59
  const bundle_properties_1 = require("./descriptions/bundle.properties");
60
+ const calendarEvent_properties_1 = require("./descriptions/calendarEvent.properties");
58
61
  const community_properties_1 = require("./descriptions/community.properties");
59
62
  const course_properties_1 = require("./descriptions/course.properties");
60
63
  const group_properties_1 = require("./descriptions/group.properties");
@@ -71,6 +74,7 @@ const ai_properties_1 = require("./descriptions/ai.properties");
71
74
  // handlers
72
75
  const apiCall_handlers_1 = require("./execute/apiCall.handlers");
73
76
  const bundle_handlers_1 = require("./execute/bundle.handlers");
77
+ const calendarEvent_handlers_1 = require("./execute/calendarEvent.handlers");
74
78
  const community_handlers_1 = require("./execute/community.handlers");
75
79
  const course_handlers_1 = require("./execute/course.handlers");
76
80
  const group_handlers_1 = require("./execute/group.handlers");
@@ -88,6 +92,7 @@ const registry = {
88
92
  ai: { ...ai_handlers_1.aiHandlers },
89
93
  apiCall: { ...apiCall_handlers_1.apiCallHandlers },
90
94
  bundle: { ...bundle_handlers_1.bundleHandlers },
95
+ calendarEvent: { ...calendarEvent_handlers_1.calendarEventHandlers },
91
96
  community: { ...community_handlers_1.communityHandlers },
92
97
  course: { ...course_handlers_1.courseHandlers },
93
98
  customFields: { ...customFields_handlers_1.customFieldsHandlers },
@@ -113,11 +118,11 @@ class LearningSuite {
113
118
  group: ['transform'],
114
119
  version: 1,
115
120
  subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
116
- description: 'Interact with LearningSuite API (powered by joergsebening.de)',
121
+ description: 'Interact with LearningSuite API (powered by agentur-systeme.de)',
117
122
  defaults: {
118
123
  name: 'LearningSuite',
119
124
  // @ts-expect-error -- description required by linter
120
- description: 'Interact with LearningSuite API (powered by joergsebening.de)',
125
+ description: 'Interact with LearningSuite API (powered by agentur-systeme.de)',
121
126
  },
122
127
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
123
128
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
@@ -130,15 +135,12 @@ class LearningSuite {
130
135
  description: 'LearningSuite API Test',
131
136
  },
132
137
  ],
133
- requestDefaults: {
134
- baseURL: '={{$credentials.baseUrl}}',
135
- headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
136
- },
137
138
  properties: [
138
139
  resource_selector_1.resourceSelector,
139
140
  ...ai_properties_1.aiProperties,
140
141
  ...apiCall_properties_1.apiCallProperties,
141
142
  ...bundle_properties_1.bundleProperties,
143
+ ...calendarEvent_properties_1.calendarEventProperties,
142
144
  ...community_properties_1.communityProperties,
143
145
  ...course_properties_1.courseProperties,
144
146
  ...customFields_properties_1.customFieldsProperties,
@@ -157,6 +159,7 @@ class LearningSuite {
157
159
  loadOptions: {
158
160
  ...loAi,
159
161
  ...loBundle,
162
+ ...loCalendarEvent,
160
163
  ...loCommunity,
161
164
  ...loCourse,
162
165
  ...loCustomFields,
@@ -170,6 +173,7 @@ class LearningSuite {
170
173
  ...loWebhook,
171
174
  },
172
175
  resourceMapping: {
176
+ getCalendarEventResourceMapperFields: calendarEvent_resourceMapper_1.getCalendarEventResourceMapperFields,
173
177
  getTemplateVariablesResourceMapperFields: hub_resourceMapper_1.getTemplateVariablesResourceMapperFields,
174
178
  getMultipleCustomFieldValueResourceMapperFields: customFields_resourceMapper_1.getMultipleCustomFieldValueResourceMapperFields,
175
179
  },
@@ -0,0 +1,18 @@
1
+ {
2
+ "node": "n8n-nodes-learningsuite.learningSuite",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["Marketing & Content", "Communication"],
6
+ "resources": {
7
+ "credentialDocumentation": [
8
+ {
9
+ "url": "https://github.com/rjsebening/n8n-nodes-learningsuite/blob/main/CREDENTIALS.md"
10
+ }
11
+ ],
12
+ "primaryDocumentation": [
13
+ {
14
+ "url": "https://github.com/rjsebening/n8n-nodes-learningsuite/blob/main/README.md"
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -242,7 +242,7 @@ function buildDesiredFilter(event) {
242
242
  filter.groupId = String(col.groupId);
243
243
  break;
244
244
  }
245
- // ---------------- Lesson Completed (kaskadiert)
245
+ // ---------------- Lesson Completed (cascading)
246
246
  case 'lesson.completed': {
247
247
  const col = getCol('additionalLessonCompleted');
248
248
  if (col.courseId)
@@ -288,12 +288,12 @@ class LearningSuiteTrigger {
288
288
  },
289
289
  group: ['trigger'],
290
290
  version: 1,
291
- description: 'Interact with LearningSuite API (powered by joergsebening.de)',
291
+ description: 'Interact with LearningSuite API (powered by agentur-systeme.de)',
292
292
  subtitle: '={{$parameter["event"]}}',
293
293
  defaults: {
294
294
  name: 'LearningSuite Trigger',
295
295
  // @ts-expect-error -- description is required by n8n node linting for defaults
296
- description: 'Interact with LearningSuite API (powered by joergsebening.de)',
296
+ description: 'Interact with LearningSuite API (powered by agentur-systeme.de)',
297
297
  },
298
298
  inputs: [],
299
299
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
@@ -0,0 +1,2 @@
1
+ import type { INodeProperties } from 'n8n-workflow';
2
+ export declare const calendarEventProperties: INodeProperties[];
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calendarEventProperties = void 0;
4
+ const RESOURCE = 'calendarEvent';
5
+ const show = (...operations) => ({
6
+ show: { resource: [RESOURCE], ...(operations.length ? { operation: operations } : {}) },
7
+ });
8
+ function availabilityFields(prefix, label, relationDescription) {
9
+ return [
10
+ {
11
+ displayName: `${label}: Relation`,
12
+ name: `${prefix}Relation`,
13
+ type: 'options',
14
+ options: [
15
+ { name: 'After Event End', value: 'afterEventEnd' },
16
+ { name: 'After Event Start', value: 'afterEventStart' },
17
+ { name: 'Before Event End', value: 'beforeEventEnd' },
18
+ { name: 'Before Event Start', value: 'beforeEventStart' },
19
+ { name: 'Not Set (Natural Start/End)', value: '' },
20
+ ],
21
+ default: '',
22
+ description: relationDescription,
23
+ },
24
+ {
25
+ displayName: `${label}: Amount`,
26
+ name: `${prefix}Amount`,
27
+ type: 'number',
28
+ typeOptions: { minValue: 0 },
29
+ default: 0,
30
+ description: 'How many units before/after the reference point. Only used when a relation is selected.',
31
+ },
32
+ {
33
+ displayName: `${label}: Unit`,
34
+ name: `${prefix}Unit`,
35
+ type: 'options',
36
+ options: [
37
+ { name: 'Minutes', value: 'minutes' },
38
+ { name: 'Hours', value: 'hours' },
39
+ { name: 'Days', value: 'days' },
40
+ ],
41
+ default: 'minutes',
42
+ description: 'Unit of the amount. Only used when a relation is selected.',
43
+ },
44
+ ];
45
+ }
46
+ function linkStageValues() {
47
+ return [
48
+ {
49
+ displayName: 'URL',
50
+ name: 'url',
51
+ type: 'string',
52
+ default: '',
53
+ required: true,
54
+ placeholder: 'https://zoom.us/j/123456789',
55
+ description: 'Link of this phase. Must be a valid http(s) URL.',
56
+ },
57
+ {
58
+ displayName: 'Enabled',
59
+ name: 'enabled',
60
+ type: 'boolean',
61
+ default: true,
62
+ description: 'Whether this phase is active',
63
+ },
64
+ {
65
+ displayName: 'Button Label',
66
+ name: 'ctaLabel',
67
+ type: 'string',
68
+ default: '',
69
+ description: 'Button text for this phase (max. 40 characters). Leave empty for the default text.',
70
+ },
71
+ {
72
+ displayName: 'Button Icon',
73
+ name: 'iconName',
74
+ type: 'string',
75
+ default: '',
76
+ description: 'Button icon name. Leave empty for the default icon.',
77
+ },
78
+ {
79
+ displayName: 'Button Color',
80
+ name: 'buttonStyle',
81
+ type: 'string',
82
+ default: '',
83
+ placeholder: '#FF0000, primary or black',
84
+ description: 'Button color as a hex code, or "primary"/"black"',
85
+ },
86
+ {
87
+ displayName: 'Status Label',
88
+ name: 'statusLabel',
89
+ type: 'string',
90
+ default: '',
91
+ placeholder: 'LIVE',
92
+ description: 'Short badge text shown on the card while this phase is active (max. 12 characters).',
93
+ },
94
+ ...availabilityFields('availableFrom', 'Available From', 'When this phase starts. Leave unset for the natural start (before: open-ended, during: event start, after: event end).'),
95
+ ...availabilityFields('availableUntil', 'Available Until', 'When this phase ends. Leave unset for the natural end (before: event start, during: event end, after: open-ended).'),
96
+ ];
97
+ }
98
+ const reminderChannelValues = [
99
+ {
100
+ displayName: 'Enabled',
101
+ name: 'enabled',
102
+ type: 'boolean',
103
+ default: true,
104
+ description: 'Whether the reminder is sent for this channel',
105
+ },
106
+ {
107
+ displayName: 'Lead Minutes',
108
+ name: 'leadMinutes',
109
+ type: 'number',
110
+ typeOptions: { minValue: 1, maxValue: 1440 },
111
+ default: 10,
112
+ description: 'How many minutes before each occurrence the reminder is sent (1-1440)',
113
+ },
114
+ ];
115
+ exports.calendarEventProperties = [
116
+ {
117
+ displayName: 'Operation',
118
+ name: 'operation',
119
+ type: 'options',
120
+ noDataExpression: true,
121
+ default: 'getAll',
122
+ displayOptions: show(),
123
+ options: [
124
+ {
125
+ name: 'Create Calendar Event',
126
+ value: 'create',
127
+ description: 'Create a calendar event series',
128
+ action: 'Create a calendar event',
129
+ },
130
+ {
131
+ name: 'Delete Calendar Event',
132
+ value: 'delete',
133
+ description: 'Delete a calendar event series including all of its occurrences',
134
+ action: 'Delete a calendar event',
135
+ },
136
+ {
137
+ name: 'Get Many',
138
+ value: 'getAll',
139
+ description: 'List calendar event series (never individual dates)',
140
+ action: 'Get many calendar events',
141
+ },
142
+ {
143
+ name: 'Update Calendar Event',
144
+ value: 'update',
145
+ description: 'Update a calendar event series (only the fields you provide)',
146
+ action: 'Update a calendar event',
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ displayName: 'From Date',
152
+ name: 'fromDate',
153
+ type: 'string',
154
+ placeholder: '2026-07-20',
155
+ displayOptions: show('getAll'),
156
+ default: '',
157
+ description: 'Start of the range (YYYY-MM-DD, inclusive). Selects series whose lifetime overlaps the range, not their individual dates; the range is widened by one day for timezone tolerance. Leave empty to return every series.',
158
+ },
159
+ {
160
+ displayName: 'To Date',
161
+ name: 'toDate',
162
+ type: 'string',
163
+ placeholder: '2026-12-21',
164
+ displayOptions: show('getAll'),
165
+ default: '',
166
+ description: 'End of the range (YYYY-MM-DD, inclusive). Only usable together with From Date; leave empty for an open-ended range.',
167
+ },
168
+ {
169
+ displayName: 'Limit',
170
+ name: 'limit',
171
+ type: 'number',
172
+ description: 'Max number of results to return',
173
+ displayOptions: show('getAll'),
174
+ typeOptions: { minValue: 1, maxValue: 100 },
175
+ default: 50,
176
+ },
177
+ {
178
+ displayName: 'Offset',
179
+ name: 'offset',
180
+ type: 'number',
181
+ displayOptions: show('getAll'),
182
+ typeOptions: { minValue: 0 },
183
+ default: 0,
184
+ description: 'Number of results to skip for pagination',
185
+ },
186
+ {
187
+ displayName: 'Calendar Event Name or ID',
188
+ name: 'calendarEventSeriesId',
189
+ type: 'options',
190
+ typeOptions: { loadOptionsMethod: 'calendarEvent_getCalendarEvents' },
191
+ displayOptions: show('update', 'delete'),
192
+ default: '',
193
+ required: true,
194
+ 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
+ },
196
+ {
197
+ displayName: 'Name',
198
+ name: 'name',
199
+ type: 'string',
200
+ displayOptions: show('create'),
201
+ default: '',
202
+ required: true,
203
+ description: 'Display name of the event, shown on the calendar card (max. 35 characters).',
204
+ },
205
+ {
206
+ displayName: 'Start Date',
207
+ name: 'startDate',
208
+ type: 'string',
209
+ placeholder: '2026-07-20',
210
+ displayOptions: show('create'),
211
+ default: '',
212
+ required: true,
213
+ description: 'Date of the first occurrence (YYYY-MM-DD), interpreted in the event time zone',
214
+ },
215
+ {
216
+ displayName: 'Start Time',
217
+ name: 'startTime',
218
+ type: 'string',
219
+ placeholder: '18:00',
220
+ displayOptions: show('create'),
221
+ default: '',
222
+ required: true,
223
+ description: 'Start time of every occurrence (HH:mm, 24-hour), interpreted in the event time zone',
224
+ },
225
+ {
226
+ displayName: 'Time Zone',
227
+ name: 'timeZone',
228
+ type: 'string',
229
+ placeholder: 'Europe/Berlin',
230
+ displayOptions: show('create'),
231
+ default: 'Europe/Berlin',
232
+ required: true,
233
+ description: 'IANA time zone for all dates/times of this event, e.g. Europe/Berlin or Europe/Vienna',
234
+ },
235
+ {
236
+ displayName: 'Repetition Interval',
237
+ name: 'repetitionInterval',
238
+ type: 'options',
239
+ displayOptions: show('create'),
240
+ options: [
241
+ { name: 'One Time', value: 'oneTime', description: 'Single occurrence on the start date' },
242
+ { name: 'Weekly', value: 'weekly', description: 'Repeats every 7 days' },
243
+ { name: 'Bi-Weekly', value: 'biWeekly', description: 'Repeats every 14 days' },
244
+ ],
245
+ default: 'oneTime',
246
+ required: true,
247
+ description: 'How often the event repeats',
248
+ },
249
+ {
250
+ displayName: 'Link',
251
+ name: 'link',
252
+ type: 'string',
253
+ placeholder: 'https://zoom.us/j/123456789',
254
+ displayOptions: show('create'),
255
+ default: '',
256
+ required: true,
257
+ description: 'The event link members join through, e.g. a Zoom or Meet URL. Must be a valid URL.',
258
+ },
259
+ {
260
+ displayName: 'Additional Fields',
261
+ name: 'additionalFields',
262
+ type: 'collection',
263
+ placeholder: 'Add Field',
264
+ default: {},
265
+ displayOptions: show('create'),
266
+ options: [
267
+ {
268
+ displayName: 'Bundle Names or IDs',
269
+ name: 'bundleIds',
270
+ type: 'multiOptions',
271
+ typeOptions: { loadOptionsMethod: 'bundle_getBundles' },
272
+ default: [],
273
+ description: 'Bundles whose owners get access to the event. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
274
+ },
275
+ {
276
+ displayName: 'Description',
277
+ name: 'description',
278
+ type: 'string',
279
+ typeOptions: { rows: 3 },
280
+ default: '',
281
+ description: 'Short description shown on the calendar card on hover',
282
+ },
283
+ {
284
+ displayName: 'Duration (Minutes)',
285
+ name: 'duration',
286
+ type: 'number',
287
+ typeOptions: { minValue: 1 },
288
+ default: 60,
289
+ description: 'Duration in minutes (e.g. 60 = one hour). Omit for open-ended events.',
290
+ },
291
+ {
292
+ displayName: 'End Date',
293
+ name: 'endDate',
294
+ type: 'string',
295
+ placeholder: '2026-12-21',
296
+ default: '',
297
+ description: 'Last date a recurring series repeats (YYYY-MM-DD, inclusive). Omit for open-ended series.',
298
+ },
299
+ {
300
+ displayName: 'Group Names or IDs',
301
+ name: 'groupIds',
302
+ type: 'multiOptions',
303
+ typeOptions: { loadOptionsMethod: 'group_getGroups' },
304
+ default: [],
305
+ description: 'Groups whose members get access to the event. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
306
+ },
307
+ {
308
+ displayName: 'Member Names or IDs',
309
+ name: 'memberIds',
310
+ type: 'multiOptions',
311
+ typeOptions: { loadOptionsMethod: 'member_getMembers' },
312
+ default: [],
313
+ description: 'Users (IDs or email addresses) that get access to the event. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
314
+ },
315
+ {
316
+ displayName: 'Priority',
317
+ name: 'priority',
318
+ type: 'number',
319
+ typeOptions: { minValue: 0, maxValue: 9 },
320
+ default: 3,
321
+ description: 'Display priority 0-9; lower numbers are shown first. Defaults to 3.',
322
+ },
323
+ ],
324
+ },
325
+ {
326
+ displayName: 'Reminders',
327
+ name: 'notificationSettings',
328
+ type: 'fixedCollection',
329
+ placeholder: 'Add Reminder Channel',
330
+ default: {},
331
+ displayOptions: show('create'),
332
+ description: 'Reminder settings of the event. Each enabled channel notifies all users with access before every occurrence. Channels you do not add send nothing.',
333
+ options: [
334
+ { displayName: 'Push', name: 'push', values: reminderChannelValues },
335
+ { displayName: 'Email', name: 'email', values: reminderChannelValues },
336
+ ],
337
+ },
338
+ {
339
+ displayName: 'Fields to Update',
340
+ name: 'updateFields',
341
+ type: 'resourceMapper',
342
+ noDataExpression: true,
343
+ required: true,
344
+ default: { mappingMode: 'defineBelow', value: null },
345
+ displayOptions: show('update'),
346
+ description: 'Fields to send to the API. Only mapped fields are changed, everything else keeps its current value. Set a field to the expression {{ null }} to clear it — the API allows that for Description, Duration and End Date only.',
347
+ typeOptions: {
348
+ resourceMapper: {
349
+ resourceMapperMethod: 'getCalendarEventResourceMapperFields',
350
+ mode: 'add',
351
+ fieldWords: { singular: 'field', plural: 'fields' },
352
+ addAllFields: false,
353
+ multiKeyMatch: false,
354
+ supportAutoMap: true,
355
+ },
356
+ },
357
+ },
358
+ {
359
+ displayName: 'Reminders',
360
+ name: 'notificationSettings',
361
+ type: 'fixedCollection',
362
+ placeholder: 'Add Reminder Channel',
363
+ default: {},
364
+ displayOptions: show('update'),
365
+ description: 'Reminder settings of the event. Leave empty to keep the current reminders; adding it replaces all channels. To stop all reminders, add both channels with Enabled turned off.',
366
+ options: [
367
+ { displayName: 'Push', name: 'push', values: reminderChannelValues },
368
+ { displayName: 'Email', name: 'email', values: reminderChannelValues },
369
+ ],
370
+ },
371
+ {
372
+ displayName: 'Link Stages',
373
+ name: 'linkStages',
374
+ type: 'fixedCollection',
375
+ placeholder: 'Add Link Stage',
376
+ default: {},
377
+ displayOptions: show('create', 'update'),
378
+ description: 'Per-phase links of the event. Requires the "During" phase; "Before"/"After" are optional and fall back to the single link. When set, link stages replace all existing phases and take precedence over the single link.',
379
+ options: [
380
+ { displayName: 'Before', name: 'before', values: linkStageValues() },
381
+ { displayName: 'During', name: 'during', values: linkStageValues() },
382
+ { displayName: 'After', name: 'after', values: linkStageValues() },
383
+ ],
384
+ },
385
+ ];
@@ -207,7 +207,7 @@ exports.memberProperties = [
207
207
  displayName: 'Do Not Require Password Change',
208
208
  name: 'doNotRequirePasswordChange',
209
209
  type: 'boolean',
210
- default: false, // ← Beispiel sagt false
210
+ default: false, // <- Example says false
211
211
  description: 'Whether the member is not prompted to change their password upon first login, if this option is enabled. This option should only be used if the member has not set a password and should log in using SSO. The default setting is "true," so for privacy reasons, only the member knows their password.',
212
212
  },
213
213
  {
@@ -30,7 +30,7 @@ exports.moduleProperties = [
30
30
  },
31
31
  ],
32
32
  },
33
- // (neu) optionaler Kurs-Filter – erleichtert die Modulauswahl
33
+ // (new) optional course filter - makes module selection easier
34
34
  {
35
35
  displayName: 'Course Name or ID',
36
36
  name: 'courseId',
@@ -22,6 +22,11 @@ exports.resourceSelector = {
22
22
  value: 'bundle',
23
23
  description: 'List bundles (course packages) and retrieve the members of a bundle',
24
24
  },
25
+ {
26
+ name: 'Calendar Event',
27
+ value: 'calendarEvent',
28
+ description: 'Manage calendar event series of the event plan',
29
+ },
25
30
  {
26
31
  name: 'Community',
27
32
  value: 'community',
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.instantProperties = void 0;
4
4
  exports.instantProperties = [
5
- // Event-Auswahl (nur Instant)
5
+ // Event selection (instant only)
6
6
  {
7
7
  displayName: 'Event',
8
8
  name: 'event',
@@ -388,7 +388,7 @@ exports.instantProperties = [
388
388
  },
389
389
  ],
390
390
  },
391
- // Feedback (nur courseInstanceId)
391
+ // Feedback (courseInstanceId only)
392
392
  {
393
393
  displayName: 'Feedback Options',
394
394
  name: 'additionalFeedbackOptions',
@@ -478,7 +478,7 @@ exports.instantProperties = [
478
478
  },
479
479
  ],
480
480
  },
481
- // Lesson Completed (kaskadierende LoadOptions)
481
+ // Lesson Completed (cascading load options)
482
482
  {
483
483
  displayName: 'Lesson Completed Options',
484
484
  name: 'additionalLessonCompleted',
@@ -136,7 +136,7 @@ exports.webhookProperties = [
136
136
  { name: 'User Activation Status Changed Events', value: 'user-activation-status-changed-events' },
137
137
  ],
138
138
  },
139
- // ====== Optionen pro Event (identisch zu instantProperties) ======
139
+ // ====== Options per event (identical to instantProperties) ======
140
140
  // Agent Action Executed
141
141
  {
142
142
  displayName: 'Agent Action Executed Options',
@@ -600,7 +600,7 @@ exports.webhookProperties = [
600
600
  },
601
601
  ],
602
602
  },
603
- // Lesson Completed (kaskadiert)
603
+ // Lesson Completed (cascading)
604
604
  {
605
605
  displayName: 'Lesson Completed Options',
606
606
  name: 'additionalLessonCompleted',
@@ -1,5 +1,5 @@
1
1
  import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
2
- /** Einheitlicher Funktions-Typ für alle Execute-Handler. */
2
+ /** Unified function type for all execute handlers. */
3
3
  export type ExecuteHandler = (ctx: IExecuteFunctions, itemIndex: number) => Promise<IDataObject | IDataObject[]>;
4
- /** Registry-Typ: resource -> operation -> handler */
4
+ /** Registry type: resource -> operation -> handler */
5
5
  export type HandlersRegistry = Record<string, Record<string, ExecuteHandler>>;
@@ -0,0 +1,7 @@
1
+ import type { ExecuteHandler } from '../exec.types';
2
+ export declare const calendarEventHandlers: {
3
+ getAll: ExecuteHandler;
4
+ create: ExecuteHandler;
5
+ update: ExecuteHandler;
6
+ delete: ExecuteHandler;
7
+ };
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calendarEventHandlers = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const shared_1 = require("../shared");
6
+ const calendarEvent_resourceMapper_1 = require("../methods/resourceMappers/calendarEvent.resourceMapper");
7
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
8
+ const TIME_OF_DAY = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
9
+ const LINK_STAGES = ['before', 'during', 'after'];
10
+ const NULLABLE_FIELDS = ['description', 'duration', 'endDate'];
11
+ function isClearedByUser(ctx, field, value) {
12
+ if (value !== null)
13
+ return false;
14
+ if (!NULLABLE_FIELDS.includes(field)) {
15
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `The API cannot clear "${field}" — only ${NULLABLE_FIELDS.join(', ')} accept null. Provide a value instead.`);
16
+ }
17
+ return true;
18
+ }
19
+ function assertIsoDate(ctx, label, value) {
20
+ if (!ISO_DATE.test(value)) {
21
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `${label} must be a date in the format YYYY-MM-DD (e.g. 2026-07-20).`);
22
+ }
23
+ }
24
+ function assertTimeOfDay(ctx, label, value) {
25
+ if (!TIME_OF_DAY.test(value)) {
26
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `${label} must be a time in the format HH:mm (e.g. 18:00).`);
27
+ }
28
+ }
29
+ /** fixedCollection entries arrive as an object or — with multipleValues — as an array. */
30
+ function firstEntry(value) {
31
+ var _a;
32
+ if (Array.isArray(value))
33
+ return (_a = value[0]) !== null && _a !== void 0 ? _a : undefined;
34
+ if (value && typeof value === 'object')
35
+ return value;
36
+ return undefined;
37
+ }
38
+ function buildAvailability(values, prefix) {
39
+ var _a, _b, _c;
40
+ const relation = String((_a = values[`${prefix}Relation`]) !== null && _a !== void 0 ? _a : '').trim();
41
+ if (!relation)
42
+ return undefined;
43
+ return {
44
+ amount: Number((_b = values[`${prefix}Amount`]) !== null && _b !== void 0 ? _b : 0),
45
+ unit: String((_c = values[`${prefix}Unit`]) !== null && _c !== void 0 ? _c : 'minutes'),
46
+ relation,
47
+ };
48
+ }
49
+ function buildLinkStage(ctx, stage, values) {
50
+ var _a, _b, _c, _d, _e;
51
+ const url = String((_a = values.url) !== null && _a !== void 0 ? _a : '').trim();
52
+ if (!url) {
53
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Link stage "${stage}" requires a URL.`);
54
+ }
55
+ const result = { url };
56
+ if (values.enabled !== undefined)
57
+ result.enabled = values.enabled;
58
+ const ctaLabel = String((_b = values.ctaLabel) !== null && _b !== void 0 ? _b : '').trim();
59
+ if (ctaLabel)
60
+ result.ctaLabel = ctaLabel;
61
+ const iconName = String((_c = values.iconName) !== null && _c !== void 0 ? _c : '').trim();
62
+ if (iconName)
63
+ result.iconName = iconName;
64
+ const buttonStyle = String((_d = values.buttonStyle) !== null && _d !== void 0 ? _d : '').trim();
65
+ if (buttonStyle)
66
+ result.color = buttonStyle;
67
+ const statusLabel = String((_e = values.statusLabel) !== null && _e !== void 0 ? _e : '').trim();
68
+ if (statusLabel)
69
+ result.statusLabel = statusLabel;
70
+ const availableFrom = buildAvailability(values, 'availableFrom');
71
+ if (availableFrom)
72
+ result.availableFrom = availableFrom;
73
+ const availableUntil = buildAvailability(values, 'availableUntil');
74
+ if (availableUntil)
75
+ result.availableUntil = availableUntil;
76
+ return result;
77
+ }
78
+ function buildLinkStages(ctx, i) {
79
+ const raw = ctx.getNodeParameter('linkStages', i, {});
80
+ const stages = {};
81
+ for (const stage of LINK_STAGES) {
82
+ const values = firstEntry(raw[stage]);
83
+ if (values)
84
+ stages[stage] = buildLinkStage(ctx, stage, values);
85
+ }
86
+ if (!Object.keys(stages).length)
87
+ return undefined;
88
+ if (!stages.during) {
89
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Link stages require the "During" phase. Add it, or remove the other phases to keep the single link.');
90
+ }
91
+ return stages;
92
+ }
93
+ function buildNotificationSettings(ctx, i) {
94
+ var _a;
95
+ const raw = ctx.getNodeParameter('notificationSettings', i, {});
96
+ const settings = {};
97
+ for (const channel of ['push', 'email']) {
98
+ const values = firstEntry(raw[channel]);
99
+ if (!values)
100
+ continue;
101
+ settings[channel] = {
102
+ enabled: values.enabled !== false,
103
+ leadMinutes: Number((_a = values.leadMinutes) !== null && _a !== void 0 ? _a : 10),
104
+ };
105
+ }
106
+ return Object.keys(settings).length ? settings : undefined;
107
+ }
108
+ const getAll = async (ctx, i) => {
109
+ const fromDate = String(ctx.getNodeParameter('fromDate', i, '') || '').trim();
110
+ const toDate = String(ctx.getNodeParameter('toDate', i, '') || '').trim();
111
+ const limit = ctx.getNodeParameter('limit', i, 50);
112
+ const offset = ctx.getNodeParameter('offset', i, 0);
113
+ if (toDate && !fromDate) {
114
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'To Date can only be used together with From Date.');
115
+ }
116
+ const qs = { limit, offset };
117
+ if (fromDate) {
118
+ assertIsoDate(ctx, 'From Date', fromDate);
119
+ qs.fromDate = fromDate;
120
+ }
121
+ if (toDate) {
122
+ assertIsoDate(ctx, 'To Date', toDate);
123
+ qs.toDate = toDate;
124
+ }
125
+ return await shared_1.lsRequest.call(ctx, 'GET', '/calendar-events', { qs });
126
+ };
127
+ const create = async (ctx, i) => {
128
+ var _a, _b;
129
+ const name = String(ctx.getNodeParameter('name', i, '') || '').trim();
130
+ const startDate = String(ctx.getNodeParameter('startDate', i, '') || '').trim();
131
+ const startTime = String(ctx.getNodeParameter('startTime', i, '') || '').trim();
132
+ const timeZone = String(ctx.getNodeParameter('timeZone', i, '') || '').trim();
133
+ const repetitionInterval = ctx.getNodeParameter('repetitionInterval', i, 'oneTime');
134
+ const link = String(ctx.getNodeParameter('link', i, '') || '').trim();
135
+ if (!name)
136
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide a name for the calendar event.');
137
+ if (!timeZone)
138
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide a time zone, e.g. Europe/Berlin.');
139
+ if (!link)
140
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide the event link.');
141
+ assertIsoDate(ctx, 'Start Date', startDate);
142
+ assertTimeOfDay(ctx, 'Start Time', startTime);
143
+ const body = { name, startDate, startTime, timeZone, repetitionInterval, link };
144
+ const additionalFields = ctx.getNodeParameter('additionalFields', i, {});
145
+ const description = String((_a = additionalFields.description) !== null && _a !== void 0 ? _a : '').trim();
146
+ if (description)
147
+ body.description = description;
148
+ if (additionalFields.priority !== undefined)
149
+ body.priority = additionalFields.priority;
150
+ if (additionalFields.duration !== undefined)
151
+ body.duration = additionalFields.duration;
152
+ const endDate = String((_b = additionalFields.endDate) !== null && _b !== void 0 ? _b : '').trim();
153
+ if (endDate) {
154
+ assertIsoDate(ctx, 'End Date', endDate);
155
+ body.endDate = endDate;
156
+ }
157
+ const memberIds = (0, shared_1.toIdArray)(additionalFields.memberIds);
158
+ if (memberIds.length)
159
+ body.memberIds = memberIds;
160
+ const groupIds = (0, shared_1.toIdArray)(additionalFields.groupIds);
161
+ if (groupIds.length)
162
+ body.groupIds = groupIds;
163
+ const bundleIds = (0, shared_1.toIdArray)(additionalFields.bundleIds);
164
+ if (bundleIds.length)
165
+ body.bundleIds = bundleIds;
166
+ const notificationSettings = buildNotificationSettings(ctx, i);
167
+ if (notificationSettings)
168
+ body.notificationSettings = notificationSettings;
169
+ const linkStages = buildLinkStages(ctx, i);
170
+ if (linkStages)
171
+ body.linkStages = linkStages;
172
+ return await shared_1.lsRequest.call(ctx, 'POST', '/calendar-events', { body });
173
+ };
174
+ const UPDATE_FIELD_LABELS = {
175
+ startDate: 'Start Date',
176
+ startTime: 'Start Time',
177
+ endDate: 'End Date',
178
+ };
179
+ const update = async (ctx, i) => {
180
+ var _a;
181
+ const calendarEventSeriesId = String(ctx.getNodeParameter('calendarEventSeriesId', i, '') || '').trim();
182
+ if (!calendarEventSeriesId) {
183
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide the ID of the calendar event.');
184
+ }
185
+ const mapped = ctx.getNodeParameter('updateFields', i, {});
186
+ const values = (_a = mapped === null || mapped === void 0 ? void 0 : mapped.value) !== null && _a !== void 0 ? _a : {};
187
+ const body = {};
188
+ for (const field of calendarEvent_resourceMapper_1.CALENDAR_EVENT_FIELD_IDS) {
189
+ if (!(field in values))
190
+ continue;
191
+ const raw = values[field];
192
+ if (raw === undefined)
193
+ continue;
194
+ if (isClearedByUser(ctx, field, raw)) {
195
+ body[field] = null;
196
+ continue;
197
+ }
198
+ if (typeof raw === 'string') {
199
+ const value = raw.trim();
200
+ if (!value)
201
+ continue;
202
+ const label = UPDATE_FIELD_LABELS[field];
203
+ if (field === 'startDate' || field === 'endDate')
204
+ assertIsoDate(ctx, label, value);
205
+ if (field === 'startTime')
206
+ assertTimeOfDay(ctx, label, value);
207
+ body[field] = value;
208
+ continue;
209
+ }
210
+ body[field] = raw;
211
+ }
212
+ const notificationSettings = buildNotificationSettings(ctx, i);
213
+ if (notificationSettings)
214
+ body.notificationSettings = notificationSettings;
215
+ const linkStages = buildLinkStages(ctx, i);
216
+ if (linkStages)
217
+ body.linkStages = linkStages;
218
+ if (!Object.keys(body).length) {
219
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide at least one field to update.');
220
+ }
221
+ return await shared_1.lsRequest.call(ctx, 'PATCH', `/calendar-events/${calendarEventSeriesId}`, { body });
222
+ };
223
+ const deleteCalendarEvent = async (ctx, i) => {
224
+ const calendarEventSeriesId = String(ctx.getNodeParameter('calendarEventSeriesId', i, '') || '').trim();
225
+ if (!calendarEventSeriesId) {
226
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Please provide the ID of the calendar event.');
227
+ }
228
+ return await shared_1.lsRequest.call(ctx, 'DELETE', `/calendar-events/${calendarEventSeriesId}`);
229
+ };
230
+ exports.calendarEventHandlers = {
231
+ getAll,
232
+ create,
233
+ update,
234
+ delete: deleteCalendarEvent,
235
+ };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registry = void 0;
4
4
  const apiCall_handlers_1 = require("./apiCall.handlers");
5
5
  const bundle_handlers_1 = require("./bundle.handlers");
6
+ const calendarEvent_handlers_1 = require("./calendarEvent.handlers");
6
7
  const community_handlers_1 = require("./community.handlers");
7
8
  const course_handlers_1 = require("./course.handlers");
8
9
  const group_handlers_1 = require("./group.handlers");
@@ -20,6 +21,7 @@ exports.registry = {
20
21
  ai: ai_handlers_1.aiHandlers,
21
22
  apiCall: apiCall_handlers_1.apiCallHandlers,
22
23
  bundle: bundle_handlers_1.bundleHandlers,
24
+ calendarEvent: calendarEvent_handlers_1.calendarEventHandlers,
23
25
  community: community_handlers_1.communityHandlers,
24
26
  course: course_handlers_1.courseHandlers,
25
27
  customFields: customFields_handlers_1.customFieldsHandlers,
@@ -179,7 +179,7 @@ function buildDesiredFilter(ctx, i, eventType) {
179
179
  filter.groupId = String(col.groupId);
180
180
  break;
181
181
  }
182
- // ---------------- Lesson Completed (kaskadiert)
182
+ // ---------------- Lesson Completed (cascading)
183
183
  case 'lesson.completed': {
184
184
  const col = getCol(ctx, i, 'additionalLessonCompleted');
185
185
  if (col.courseId)
@@ -0,0 +1,5 @@
1
+ import type { ILoadOptionsFunctions } from 'n8n-workflow';
2
+ export declare function calendarEvent_getCalendarEvents(this: ILoadOptionsFunctions): Promise<{
3
+ name: string;
4
+ value: string;
5
+ }[]>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calendarEvent_getCalendarEvents = calendarEvent_getCalendarEvents;
4
+ const shared_1 = require("../../shared");
5
+ const common_1 = require("./common");
6
+ async function calendarEvent_getCalendarEvents() {
7
+ const res = await shared_1.lsRequest.call(this, 'GET', '/calendar-events', { qs: { limit: 100 } });
8
+ const rows = (0, common_1.ensureArray)(res);
9
+ return rows.map((e) => {
10
+ var _a, _b;
11
+ const when = [e.startDate, e.startTime].filter(Boolean).join(' ');
12
+ const label = e.name ? `${e.name}${when ? ` — ${when}` : ''}` : ((_a = e.id) !== null && _a !== void 0 ? _a : 'Unknown');
13
+ return { name: String(label), value: String((_b = e.id) !== null && _b !== void 0 ? _b : label) };
14
+ });
15
+ }
@@ -6,7 +6,7 @@ const shared_1 = require("../../shared");
6
6
  const common_1 = require("./common");
7
7
  function buildLabel(m) {
8
8
  var _a, _b;
9
- const fullName = (m === null || m === void 0 ? void 0 : m.fullName) || [m === null || m === void 0 ? void 0 : m.firstName, m === null || m === void 0 ? void 0 : m.lastName].filter(Boolean).join(' ') || 'Unbekannt';
9
+ const fullName = (m === null || m === void 0 ? void 0 : m.fullName) || [m === null || m === void 0 ? void 0 : m.firstName, m === null || m === void 0 ? void 0 : m.lastName].filter(Boolean).join(' ') || 'Unknown';
10
10
  const role = (_a = m === null || m === void 0 ? void 0 : m.roleId) !== null && _a !== void 0 ? _a : '—';
11
11
  const email = (_b = m === null || m === void 0 ? void 0 : m.email) !== null && _b !== void 0 ? _b : '';
12
12
  return [fullName, role, email].filter(Boolean).join(' - ');
@@ -0,0 +1,3 @@
1
+ import type { ILoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow';
2
+ export declare const CALENDAR_EVENT_FIELD_IDS: string[];
3
+ export declare function getCalendarEventResourceMapperFields(this: ILoadOptionsFunctions): Promise<ResourceMapperFields>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CALENDAR_EVENT_FIELD_IDS = void 0;
4
+ exports.getCalendarEventResourceMapperFields = getCalendarEventResourceMapperFields;
5
+ const CALENDAR_EVENT_FIELDS = [
6
+ { id: 'name', displayName: 'Name (Max. 35 Characters)', type: 'string' },
7
+ { id: 'description', displayName: 'Description', type: 'string' },
8
+ { id: 'priority', displayName: 'Priority (0-9, Lower Shows First)', type: 'number' },
9
+ { id: 'startDate', displayName: 'Start Date (YYYY-MM-DD)', type: 'string' },
10
+ { id: 'startTime', displayName: 'Start Time (HH:mm)', type: 'string' },
11
+ { id: 'duration', displayName: 'Duration (Minutes)', type: 'number' },
12
+ { id: 'timeZone', displayName: 'Time Zone (IANA, e.g. Europe/Berlin)', type: 'string' },
13
+ {
14
+ id: 'repetitionInterval',
15
+ displayName: 'Repetition Interval',
16
+ type: 'options',
17
+ options: [
18
+ { name: 'One Time', value: 'oneTime' },
19
+ { name: 'Weekly', value: 'weekly' },
20
+ { name: 'Bi-Weekly', value: 'biWeekly' },
21
+ ],
22
+ },
23
+ { id: 'endDate', displayName: 'End Date (YYYY-MM-DD)', type: 'string' },
24
+ { id: 'link', displayName: 'Link (Event URL)', type: 'string' },
25
+ ].map((field) => ({
26
+ ...field,
27
+ required: false,
28
+ display: true,
29
+ defaultMatch: false,
30
+ canBeUsedToMatch: false,
31
+ }));
32
+ exports.CALENDAR_EVENT_FIELD_IDS = CALENDAR_EVENT_FIELDS.map((field) => field.id);
33
+ async function getCalendarEventResourceMapperFields() {
34
+ return { fields: CALENDAR_EVENT_FIELDS };
35
+ }
@@ -149,12 +149,12 @@ class LearningSuitePollingTrigger {
149
149
  group: ['trigger', 'schedule'],
150
150
  version: 1,
151
151
  polling: true,
152
- description: 'Polling Trigger node for LearningSuite API (powered by joergsebening.de)',
152
+ description: 'Polling Trigger node for LearningSuite API (powered by agentur-systeme.de)',
153
153
  subtitle: '={{$parameter["event"]}}',
154
154
  defaults: {
155
155
  name: 'LearningSuite Polling Trigger',
156
156
  // @ts-expect-error -- description is required by n8n node linting for defaults
157
- description: 'Polling Trigger node for LearningSuite API (powered by joergsebening.de)',
157
+ description: 'Polling Trigger node for LearningSuite API (powered by agentur-systeme.de)',
158
158
  },
159
159
  inputs: [],
160
160
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rjsebening/n8n-nodes-learningsuite",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "n8n node for LearningSuite API",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",