@memberjunction/integration-connectors 5.51.0 → 6.1.0-edge.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.
Files changed (155) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +109 -277
  3. package/dist/index.d.ts +0 -36
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +7 -38
  6. package/dist/index.js.map +1 -1
  7. package/package.json +14 -19
  8. package/dist/AptifyConnector.d.ts +0 -95
  9. package/dist/AptifyConnector.d.ts.map +0 -1
  10. package/dist/AptifyConnector.js +0 -760
  11. package/dist/AptifyConnector.js.map +0 -1
  12. package/dist/BlackbaudConnector.d.ts +0 -64
  13. package/dist/BlackbaudConnector.d.ts.map +0 -1
  14. package/dist/BlackbaudConnector.js +0 -916
  15. package/dist/BlackbaudConnector.js.map +0 -1
  16. package/dist/ConstantContactConnector.d.ts +0 -137
  17. package/dist/ConstantContactConnector.d.ts.map +0 -1
  18. package/dist/ConstantContactConnector.js +0 -663
  19. package/dist/ConstantContactConnector.js.map +0 -1
  20. package/dist/CventConnector.d.ts +0 -146
  21. package/dist/CventConnector.d.ts.map +0 -1
  22. package/dist/CventConnector.js +0 -540
  23. package/dist/CventConnector.js.map +0 -1
  24. package/dist/DynamicsDataverseConnector.d.ts +0 -296
  25. package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
  26. package/dist/DynamicsDataverseConnector.js +0 -790
  27. package/dist/DynamicsDataverseConnector.js.map +0 -1
  28. package/dist/FileFeedConnector.d.ts +0 -64
  29. package/dist/FileFeedConnector.d.ts.map +0 -1
  30. package/dist/FileFeedConnector.js +0 -190
  31. package/dist/FileFeedConnector.js.map +0 -1
  32. package/dist/FontevaConnector.d.ts +0 -153
  33. package/dist/FontevaConnector.d.ts.map +0 -1
  34. package/dist/FontevaConnector.js +0 -421
  35. package/dist/FontevaConnector.js.map +0 -1
  36. package/dist/GrowthZoneConnector.d.ts +0 -183
  37. package/dist/GrowthZoneConnector.d.ts.map +0 -1
  38. package/dist/GrowthZoneConnector.js +0 -702
  39. package/dist/GrowthZoneConnector.js.map +0 -1
  40. package/dist/HivebriteConnector.d.ts +0 -151
  41. package/dist/HivebriteConnector.d.ts.map +0 -1
  42. package/dist/HivebriteConnector.js +0 -533
  43. package/dist/HivebriteConnector.js.map +0 -1
  44. package/dist/HubSpotConnector.d.ts +0 -523
  45. package/dist/HubSpotConnector.d.ts.map +0 -1
  46. package/dist/HubSpotConnector.js +0 -3110
  47. package/dist/HubSpotConnector.js.map +0 -1
  48. package/dist/IMISConnector.d.ts +0 -171
  49. package/dist/IMISConnector.d.ts.map +0 -1
  50. package/dist/IMISConnector.js +0 -695
  51. package/dist/IMISConnector.js.map +0 -1
  52. package/dist/MJToMJConnector.d.ts +0 -279
  53. package/dist/MJToMJConnector.d.ts.map +0 -1
  54. package/dist/MJToMJConnector.js +0 -893
  55. package/dist/MJToMJConnector.js.map +0 -1
  56. package/dist/MagnetMailConnector.d.ts +0 -163
  57. package/dist/MagnetMailConnector.d.ts.map +0 -1
  58. package/dist/MagnetMailConnector.js +0 -823
  59. package/dist/MagnetMailConnector.js.map +0 -1
  60. package/dist/MailchimpConnector.d.ts +0 -197
  61. package/dist/MailchimpConnector.d.ts.map +0 -1
  62. package/dist/MailchimpConnector.js +0 -791
  63. package/dist/MailchimpConnector.js.map +0 -1
  64. package/dist/MemberSuiteConnector.d.ts +0 -185
  65. package/dist/MemberSuiteConnector.d.ts.map +0 -1
  66. package/dist/MemberSuiteConnector.js +0 -762
  67. package/dist/MemberSuiteConnector.js.map +0 -1
  68. package/dist/NeonCRMConnector.d.ts +0 -273
  69. package/dist/NeonCRMConnector.d.ts.map +0 -1
  70. package/dist/NeonCRMConnector.js +0 -1039
  71. package/dist/NeonCRMConnector.js.map +0 -1
  72. package/dist/NetForumConnector.d.ts +0 -170
  73. package/dist/NetForumConnector.d.ts.map +0 -1
  74. package/dist/NetForumConnector.js +0 -778
  75. package/dist/NetForumConnector.js.map +0 -1
  76. package/dist/NetSuiteConnector.d.ts +0 -136
  77. package/dist/NetSuiteConnector.d.ts.map +0 -1
  78. package/dist/NetSuiteConnector.js +0 -703
  79. package/dist/NetSuiteConnector.js.map +0 -1
  80. package/dist/NimbleAMSConnector.d.ts +0 -142
  81. package/dist/NimbleAMSConnector.d.ts.map +0 -1
  82. package/dist/NimbleAMSConnector.js +0 -703
  83. package/dist/NimbleAMSConnector.js.map +0 -1
  84. package/dist/NoviConnector.d.ts +0 -178
  85. package/dist/NoviConnector.d.ts.map +0 -1
  86. package/dist/NoviConnector.js +0 -566
  87. package/dist/NoviConnector.js.map +0 -1
  88. package/dist/ORCIDConnector.d.ts +0 -207
  89. package/dist/ORCIDConnector.d.ts.map +0 -1
  90. package/dist/ORCIDConnector.js +0 -708
  91. package/dist/ORCIDConnector.js.map +0 -1
  92. package/dist/OpenWaterConnector.d.ts +0 -173
  93. package/dist/OpenWaterConnector.d.ts.map +0 -1
  94. package/dist/OpenWaterConnector.js +0 -893
  95. package/dist/OpenWaterConnector.js.map +0 -1
  96. package/dist/PathLMSConnector.d.ts +0 -395
  97. package/dist/PathLMSConnector.d.ts.map +0 -1
  98. package/dist/PathLMSConnector.js +0 -1281
  99. package/dist/PathLMSConnector.js.map +0 -1
  100. package/dist/PheedLoopConnector.d.ts +0 -119
  101. package/dist/PheedLoopConnector.d.ts.map +0 -1
  102. package/dist/PheedLoopConnector.js +0 -429
  103. package/dist/PheedLoopConnector.js.map +0 -1
  104. package/dist/PropFuelConnector.d.ts +0 -153
  105. package/dist/PropFuelConnector.d.ts.map +0 -1
  106. package/dist/PropFuelConnector.js +0 -450
  107. package/dist/PropFuelConnector.js.map +0 -1
  108. package/dist/QuickBooksConnector.d.ts +0 -92
  109. package/dist/QuickBooksConnector.d.ts.map +0 -1
  110. package/dist/QuickBooksConnector.js +0 -713
  111. package/dist/QuickBooksConnector.js.map +0 -1
  112. package/dist/RasaConnector.d.ts +0 -152
  113. package/dist/RasaConnector.d.ts.map +0 -1
  114. package/dist/RasaConnector.js +0 -929
  115. package/dist/RasaConnector.js.map +0 -1
  116. package/dist/Reach360Connector.d.ts +0 -175
  117. package/dist/Reach360Connector.d.ts.map +0 -1
  118. package/dist/Reach360Connector.js +0 -796
  119. package/dist/Reach360Connector.js.map +0 -1
  120. package/dist/RelationalDBConnector.d.ts +0 -108
  121. package/dist/RelationalDBConnector.d.ts.map +0 -1
  122. package/dist/RelationalDBConnector.js +0 -228
  123. package/dist/RelationalDBConnector.js.map +0 -1
  124. package/dist/RhythmConnector.d.ts +0 -188
  125. package/dist/RhythmConnector.d.ts.map +0 -1
  126. package/dist/RhythmConnector.js +0 -784
  127. package/dist/RhythmConnector.js.map +0 -1
  128. package/dist/SageIntacctConnector.d.ts +0 -337
  129. package/dist/SageIntacctConnector.d.ts.map +0 -1
  130. package/dist/SageIntacctConnector.js +0 -2081
  131. package/dist/SageIntacctConnector.js.map +0 -1
  132. package/dist/SalesforceConnector.d.ts +0 -461
  133. package/dist/SalesforceConnector.d.ts.map +0 -1
  134. package/dist/SalesforceConnector.js +0 -1862
  135. package/dist/SalesforceConnector.js.map +0 -1
  136. package/dist/SharePointConnector.d.ts +0 -188
  137. package/dist/SharePointConnector.d.ts.map +0 -1
  138. package/dist/SharePointConnector.js +0 -716
  139. package/dist/SharePointConnector.js.map +0 -1
  140. package/dist/WicketConnector.d.ts +0 -252
  141. package/dist/WicketConnector.d.ts.map +0 -1
  142. package/dist/WicketConnector.js +0 -938
  143. package/dist/WicketConnector.js.map +0 -1
  144. package/dist/WildApricotConnector.d.ts +0 -201
  145. package/dist/WildApricotConnector.d.ts.map +0 -1
  146. package/dist/WildApricotConnector.js +0 -1129
  147. package/dist/WildApricotConnector.js.map +0 -1
  148. package/dist/YourMembershipConnector.d.ts +0 -349
  149. package/dist/YourMembershipConnector.d.ts.map +0 -1
  150. package/dist/YourMembershipConnector.js +0 -4062
  151. package/dist/YourMembershipConnector.js.map +0 -1
  152. package/dist/generate-integration-actions.d.ts +0 -23
  153. package/dist/generate-integration-actions.d.ts.map +0 -1
  154. package/dist/generate-integration-actions.js +0 -201
  155. package/dist/generate-integration-actions.js.map +0 -1
@@ -1,796 +0,0 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { RegisterClass } from '@memberjunction/global';
8
- import { Metadata } from '@memberjunction/core';
9
- import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
10
- // ─── Constants ────────────────────────────────────────────────────────
11
- const REACH360_BASE_URL = 'https://api.reach360.com';
12
- /** Default max retries when a request fails with a retryable error. */
13
- const DEFAULT_MAX_RETRIES = 3;
14
- /** Default HTTP request timeout (ms). */
15
- const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
16
- /** Minimum interval between outbound requests (ms) — Reach 360 rate limits are undocumented. */
17
- const DEFAULT_MIN_REQUEST_INTERVAL_MS = 250;
18
- /** Default page size when the caller does not specify one — Reach 360 uses 50. Max 100. */
19
- const DEFAULT_PAGE_SIZE = 50;
20
- /** Absolute ceiling for requested page size per the documented API. */
21
- const MAX_PAGE_SIZE = 100;
22
- // ─── Static Action Metadata ───────────────────────────────────────────
23
- /**
24
- * Objects exposed through the action generator. Mirrors the runtime metadata
25
- * defined in `metadata/integrations/.reach360.json` but is maintained here so
26
- * action generation does not require a live connection.
27
- */
28
- const REACH360_OBJECTS = [
29
- {
30
- Name: 'Courses', DisplayName: 'Courses',
31
- Description: 'Courses authored in Articulate Rise/Storyline/Microlearning or imported SCORM/xAPI content.',
32
- SupportsWrite: false,
33
- Fields: [
34
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Unique identifier of the course' },
35
- { Name: 'title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Course title' },
36
- { Name: 'contentType', DisplayName: 'Content Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Content source (rise/storyline/microlearning/imported)' },
37
- { Name: 'coverImageUrl', DisplayName: 'Cover Image URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'URL for course cover image' },
38
- { Name: 'authorUrl', DisplayName: 'Author URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'HATEOAS URL for course author' },
39
- { Name: 'url', DisplayName: 'URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Canonical URL for this course' },
40
- ],
41
- },
42
- {
43
- Name: 'Users', DisplayName: 'Users',
44
- Description: 'Reach 360 users — created via invitation or SSO provisioning.',
45
- SupportsWrite: false,
46
- Fields: [
47
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Unique identifier of the user' },
48
- { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'User email — natural key' },
49
- { Name: 'role', DisplayName: 'Role', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'learner | author | admin' },
50
- { Name: 'firstName', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'User first name' },
51
- { Name: 'lastName', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'User last name' },
52
- { Name: 'lastActiveAt', DisplayName: 'Last Active At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last platform activity timestamp' },
53
- { Name: 'articulate360User', DisplayName: 'Articulate 360 User', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Managed by Articulate 360 Teams subscription' },
54
- ],
55
- },
56
- {
57
- Name: 'Groups', DisplayName: 'Groups',
58
- Description: 'Groups used for user tagging/categorization and course assignment. UI-created; API supports membership mutations.',
59
- SupportsWrite: false,
60
- Fields: [
61
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Unique identifier of the group' },
62
- { Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Group name' },
63
- { Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Group description' },
64
- { Name: 'usersUrl', DisplayName: 'Users URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'HATEOAS URL for group members' },
65
- { Name: 'coursesUrl', DisplayName: 'Courses URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'HATEOAS URL for group courses' },
66
- ],
67
- },
68
- {
69
- Name: 'LearningPaths', DisplayName: 'Learning Paths',
70
- Description: 'Ordered sequences of courses — created in the UI; enrollments manageable via API.',
71
- SupportsWrite: false,
72
- Fields: [
73
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Unique identifier of the learning path' },
74
- { Name: 'title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Learning path title' },
75
- { Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Learning path description' },
76
- ],
77
- },
78
- {
79
- Name: 'ActivityReport', DisplayName: 'Activity Report',
80
- Description: 'Account-wide session activity feed — primary source for incremental engagement data.',
81
- SupportsWrite: false,
82
- Fields: [
83
- { Name: 'courseId', DisplayName: 'Course ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Course identifier' },
84
- { Name: 'userId', DisplayName: 'User ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'User identifier' },
85
- { Name: 'status', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Session status' },
86
- { Name: 'startedAt', DisplayName: 'Started At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Session start timestamp' },
87
- { Name: 'completedAt', DisplayName: 'Completed At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Session completion timestamp (watermark field)' },
88
- { Name: 'durationSeconds', DisplayName: 'Duration', Type: 'integer', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Session active duration in seconds' },
89
- { Name: 'quizScore', DisplayName: 'Quiz Score', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Quiz score when applicable' },
90
- ],
91
- },
92
- ];
93
- /**
94
- * Maps lowercase object names to the top-level response key that holds the
95
- * record array for list endpoints. Reach 360 wraps every list response under
96
- * a plural key (`courses`, `users`, `sessions`, etc.) alongside `nextUrl`.
97
- */
98
- const REACH360_RESPONSE_DATA_KEYS = {
99
- courses: 'courses',
100
- users: 'users',
101
- groups: 'groups',
102
- learningpaths: 'learningPaths',
103
- activityreport: 'sessions',
104
- };
105
- /** Maps lowercase object names to their list API paths (relative to base URL). */
106
- const REACH360_API_PATHS = {
107
- courses: '/courses',
108
- users: '/users',
109
- groups: '/groups',
110
- learningpaths: '/learning-paths',
111
- activityreport: '/reports/activity',
112
- };
113
- /**
114
- * Provable implicit foreign keys, keyed by lowercase object name → (field name →
115
- * referenced object's external name). Each entry is proven by a `<Sibling>Id`
116
- * field whose `<Sibling>` resolves (singular/plural-aware) to ANOTHER object
117
- * declared in REACH360_OBJECTS:
118
- * - ActivityReport.courseId → Courses (composite-PK part; also the FK)
119
- * - ActivityReport.userId → Users (composite-PK part; also the FK)
120
- * Both are part of ActivityReport's composite {courseId, userId} primary key —
121
- * the association/junction pattern — so each is simultaneously PK and FK.
122
- */
123
- const REACH360_FOREIGN_KEYS = {
124
- activityreport: {
125
- courseId: 'Courses',
126
- userId: 'Users',
127
- },
128
- };
129
- /**
130
- * Promotes each object's OWN declared "last changed" timestamp into the
131
- * IncrementalWatermarkField slot. These are the exact fields the connector
132
- * already filters on client-side in `watermarkFieldFor()` / `applyClientWatermark()`:
133
- * - ActivityReport → completedAt (declared "Session completion timestamp (watermark field)")
134
- * - Users → lastActiveAt (declared "Last platform activity timestamp")
135
- * Objects with no declared change-timestamp are intentionally absent (honest gap).
136
- */
137
- const REACH360_WATERMARK_FIELDS = {
138
- activityreport: 'completedAt',
139
- users: 'lastActiveAt',
140
- };
141
- // ─── Connector Implementation ─────────────────────────────────────────
142
- /**
143
- * Connector for the Reach 360 LMS REST API.
144
- *
145
- * Reach 360 is Articulate's lightweight cloud LMS. The API is documented only
146
- * as HTML articles (no OpenAPI spec) and exposes ~25 endpoints across Courses,
147
- * Users, Groups, Learning Paths, Reports, Invitations and Completions.
148
- *
149
- * Auth: a single static Bearer token generated in the Reach 360 admin UI. No
150
- * OAuth flow, no refresh tokens. The token is sent on every request as
151
- * `Authorization: Bearer {apiKey}`.
152
- *
153
- * Pagination: cursor-based. List responses include a `nextUrl` pointer that
154
- * encodes the next cursor; follow until null.
155
- *
156
- * Incremental sync: Reach 360 has no CDC endpoint and no server-side date
157
- * filtering on list endpoints. Instead, `/reports/activity` acts as a
158
- * session-level change stream; callers filter client-side by `completedAt`.
159
- */
160
- let Reach360Connector = class Reach360Connector extends BaseRESTIntegrationConnector {
161
- constructor() {
162
- super(...arguments);
163
- /** Timestamp of the last HTTP request — used for throttling. */
164
- this.lastRequestTime = 0;
165
- }
166
- // ── Capability getters ────────────────────────────────────────────
167
- get SupportsCreate() { return true; } // Invitations, Completions
168
- get SupportsUpdate() { return true; } // Groups update via PUT /groups/{groupId}; other primary resources still return 400 in UpdateRecord
169
- get SupportsDelete() { return true; } // User delete, group membership removal
170
- get IntegrationName() { return 'Reach360'; }
171
- // ── Action generation ─────────────────────────────────────────────
172
- /**
173
- * Returns the static object catalog for action generation. Derived from
174
- * Articulate's documented endpoint catalog (no OpenAPI spec exists).
175
- */
176
- GetIntegrationObjects() {
177
- return REACH360_OBJECTS;
178
- }
179
- GetActionGeneratorConfig() {
180
- const objects = this.GetIntegrationObjects();
181
- if (objects.length === 0)
182
- return null;
183
- return {
184
- IntegrationName: 'Reach360',
185
- CategoryName: 'Reach360',
186
- IconClass: 'fa-solid fa-graduation-cap',
187
- Objects: objects,
188
- IncludeSearch: false,
189
- IncludeList: false,
190
- CategoryDescription: 'Reach 360 LMS integration actions',
191
- ParentCategoryName: 'Learning',
192
- };
193
- }
194
- GetDefaultConfiguration() {
195
- return {
196
- DefaultSchemaName: 'Reach360',
197
- DefaultObjects: [
198
- {
199
- SourceObjectName: 'Users',
200
- TargetTableName: 'Reach360_User',
201
- TargetEntityName: 'Reach360 Users',
202
- SyncEnabled: true,
203
- FieldMappings: this.GetDefaultFieldMappings('Users', 'Contacts'),
204
- },
205
- {
206
- SourceObjectName: 'Courses',
207
- TargetTableName: 'Reach360_Course',
208
- TargetEntityName: 'Reach360 Courses',
209
- SyncEnabled: true,
210
- FieldMappings: this.GetDefaultFieldMappings('Courses', 'Courses'),
211
- },
212
- {
213
- SourceObjectName: 'ActivityReport',
214
- TargetTableName: 'Reach360_Activity',
215
- TargetEntityName: 'Reach360 Activity',
216
- SyncEnabled: true,
217
- FieldMappings: [],
218
- },
219
- ],
220
- };
221
- }
222
- GetDefaultFieldMappings(objectName, _entityName) {
223
- switch (objectName) {
224
- case 'Users':
225
- return [
226
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
227
- { SourceFieldName: 'email', DestinationFieldName: 'Email' },
228
- { SourceFieldName: 'firstName', DestinationFieldName: 'FirstName' },
229
- { SourceFieldName: 'lastName', DestinationFieldName: 'LastName' },
230
- ];
231
- case 'Courses':
232
- return [
233
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
234
- { SourceFieldName: 'title', DestinationFieldName: 'Title' },
235
- ];
236
- default:
237
- return [];
238
- }
239
- }
240
- // ─── TestConnection ──────────────────────────────────────────────
241
- /**
242
- * Verifies connectivity by hitting GET /users?limit=1. A 2xx response with
243
- * a recognizable envelope confirms both credentials and rate-limit posture.
244
- */
245
- async TestConnection(companyIntegration, contextUser) {
246
- try {
247
- const auth = await this.Authenticate(companyIntegration, contextUser);
248
- const url = `${REACH360_BASE_URL}/users?limit=1`;
249
- const headers = this.BuildHeaders(auth);
250
- const resp = await this.MakeHTTPRequest(auth, url, 'GET', headers);
251
- if (resp.Status < 200 || resp.Status >= 300) {
252
- return { Success: false, Message: `Reach360 TestConnection failed: HTTP ${resp.Status}` };
253
- }
254
- return {
255
- Success: true,
256
- Message: 'Successfully connected to Reach 360',
257
- ServerVersion: 'Reach 360 REST API v1',
258
- };
259
- }
260
- catch (err) {
261
- const message = err instanceof Error ? err.message : String(err);
262
- return { Success: false, Message: `Connection failed: ${message}` };
263
- }
264
- }
265
- // ─── Discovery ────────────────────────────────────────────────────
266
- /**
267
- * Overrides the base cache-driven discovery so the connector is usable
268
- * before the integration metadata has been seeded into the DB (e.g. at
269
- * first setup). Returns the static REACH360_OBJECTS catalog.
270
- */
271
- async DiscoverObjects(_companyIntegration, _contextUser) {
272
- return REACH360_OBJECTS.map(obj => ({
273
- Name: obj.Name,
274
- Label: obj.DisplayName,
275
- Description: obj.Description,
276
- SupportsIncrementalSync: obj.Name === 'ActivityReport' || obj.Name === 'Users',
277
- SupportsWrite: obj.SupportsWrite,
278
- }));
279
- }
280
- /**
281
- * Returns the static field list for the named object. Reach 360 has no
282
- * custom-field system, so there is no runtime enrichment to perform.
283
- */
284
- async DiscoverFields(_companyIntegration, objectName, _contextUser) {
285
- const obj = REACH360_OBJECTS.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
286
- if (!obj)
287
- return [];
288
- const fkMap = REACH360_FOREIGN_KEYS[objectName.toLowerCase()] ?? {};
289
- return obj.Fields.map(f => {
290
- const fkTarget = fkMap[f.Name];
291
- return {
292
- Name: f.Name,
293
- Label: f.DisplayName,
294
- Description: f.Description,
295
- DataType: f.Type,
296
- IsRequired: f.IsRequired,
297
- IsPrimaryKey: f.IsPrimaryKey,
298
- IsUniqueKey: f.IsPrimaryKey,
299
- IsReadOnly: f.IsReadOnly,
300
- IsForeignKey: fkTarget != null,
301
- ForeignKeyTarget: fkTarget ?? null,
302
- };
303
- });
304
- }
305
- /**
306
- * Builds the source-schema introspection result from the static
307
- * REACH360_OBJECTS catalog. Overrides the base implementation purely to
308
- * additionally surface each object's IncrementalWatermarkField — the base
309
- * IntrospectSchema derives PK/FK from DiscoverFields() but does not emit a
310
- * watermark. All PK/FK/watermark values here are promoted from this
311
- * connector's OWN declared metadata (composite-PK association fields and the
312
- * declared change-timestamps); nothing is inferred from a live API.
313
- */
314
- async IntrospectSchema(companyIntegration, contextUser, options) {
315
- const wanted = options?.ObjectNames && options.ObjectNames.length > 0
316
- ? new Set(options.ObjectNames.map(n => n.toLowerCase()))
317
- : null;
318
- const objects = wanted
319
- ? REACH360_OBJECTS.filter(o => wanted.has(o.Name.toLowerCase()))
320
- : REACH360_OBJECTS;
321
- const result = { Objects: [] };
322
- for (const obj of objects) {
323
- const fields = await this.DiscoverFields(companyIntegration, obj.Name, contextUser);
324
- const sourceFields = fields.map(f => ({
325
- Name: f.Name,
326
- Label: f.Label,
327
- Description: f.Description,
328
- SourceType: f.DataType,
329
- IsRequired: f.IsRequired,
330
- AllowsNull: f.AllowsNull,
331
- MaxLength: f.MaxLength ?? null,
332
- Precision: f.Precision ?? null,
333
- Scale: f.Scale ?? null,
334
- DefaultValue: f.DefaultValue ?? null,
335
- IsPrimaryKey: f.IsPrimaryKey ?? false,
336
- IsUniqueKey: f.IsUniqueKey,
337
- IsReadOnly: f.IsReadOnly,
338
- IsForeignKey: f.IsForeignKey ?? false,
339
- ForeignKeyTarget: f.ForeignKeyTarget ?? null,
340
- }));
341
- const relationships = sourceFields
342
- .filter(f => f.IsForeignKey && f.ForeignKeyTarget)
343
- .map(f => ({ FieldName: f.Name, TargetObject: f.ForeignKeyTarget, TargetField: 'ID' }));
344
- result.Objects.push({
345
- ExternalName: obj.Name,
346
- ExternalLabel: obj.DisplayName,
347
- Description: obj.Description,
348
- Fields: sourceFields,
349
- PrimaryKeyFields: sourceFields.filter(f => f.IsPrimaryKey).map(f => f.Name),
350
- Relationships: relationships,
351
- IncrementalWatermarkField: REACH360_WATERMARK_FIELDS[obj.Name.toLowerCase()],
352
- });
353
- }
354
- return result;
355
- }
356
- // ─── FetchChanges (cursor pagination) ────────────────────────────
357
- /**
358
- * Reach 360 uses cursor pagination via `nextUrl`. Fetches the configured
359
- * page size, follows the cursor chain until either `nextUrl` is null or
360
- * `BatchSize` is reached.
361
- *
362
- * For `ActivityReport` and `Users` objects the caller's watermark is
363
- * applied client-side against `completedAt` / `lastActiveAt` respectively.
364
- */
365
- async FetchChanges(ctx) {
366
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
367
- const objLower = ctx.ObjectName.toLowerCase();
368
- const path = REACH360_API_PATHS[objLower];
369
- if (!path) {
370
- console.warn(`[Reach360] No API path mapped for object "${ctx.ObjectName}"`);
371
- return { Records: [], HasMore: false };
372
- }
373
- const dataKey = REACH360_RESPONSE_DATA_KEYS[objLower] ?? objLower;
374
- const pageSize = Math.min(ctx.BatchSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
375
- const initialUrl = ctx.CurrentCursor
376
- ? this.DecodeCursor(ctx.CurrentCursor)
377
- : `${REACH360_BASE_URL}${path}?limit=${pageSize}`;
378
- const { records, nextCursor } = await this.fetchCursorBatch(auth, initialUrl, dataKey, ctx.BatchSize);
379
- const { filtered, newWatermark } = this.applyClientWatermark(records, objLower, ctx.WatermarkValue);
380
- const externalRecords = filtered.map(r => this.toExternalRecord(r, ctx.ObjectName));
381
- return {
382
- Records: externalRecords,
383
- HasMore: nextCursor != null,
384
- NextCursor: nextCursor ?? undefined,
385
- NewWatermarkValue: !nextCursor && newWatermark ? newWatermark : undefined,
386
- };
387
- }
388
- /**
389
- * Walks the `nextUrl` chain starting from `startUrl` and accumulates up to
390
- * `batchSize` records. Returns the raw record array and the final cursor
391
- * (base64-encoded `nextUrl` when more remain, or null).
392
- */
393
- async fetchCursorBatch(auth, startUrl, dataKey, batchSize) {
394
- const all = [];
395
- let url = startUrl;
396
- const limit = batchSize > 0 ? batchSize : Number.MAX_SAFE_INTEGER;
397
- while (url && all.length < limit) {
398
- const headers = this.BuildHeaders(auth);
399
- const resp = await this.MakeHTTPRequest(auth, url, 'GET', headers);
400
- if (resp.Status < 200 || resp.Status >= 300) {
401
- throw new Error(`Reach360 GET ${url} returned HTTP ${resp.Status}`);
402
- }
403
- const body = (resp.Body ?? {});
404
- const arr = body[dataKey];
405
- if (Array.isArray(arr)) {
406
- all.push(...arr);
407
- }
408
- const next = typeof body.nextUrl === 'string' && body.nextUrl.length > 0 ? body.nextUrl : null;
409
- if (!next || all.length >= limit) {
410
- return { records: all.slice(0, limit), nextCursor: all.length >= limit && next ? this.EncodeCursor(next) : null };
411
- }
412
- url = next;
413
- }
414
- return { records: all, nextCursor: null };
415
- }
416
- /**
417
- * Applies a client-side watermark filter against the appropriate date
418
- * field for the given object. Returns records newer than the watermark
419
- * plus the latest observed timestamp (to update the watermark after the
420
- * final page).
421
- */
422
- applyClientWatermark(records, objLower, watermark) {
423
- const dateField = this.watermarkFieldFor(objLower);
424
- if (!dateField)
425
- return { filtered: records, newWatermark: null };
426
- const watermarkDate = watermark ? new Date(watermark) : null;
427
- let latest = null;
428
- const filtered = [];
429
- for (const rec of records) {
430
- const raw = rec[dateField];
431
- if (typeof raw !== 'string' || raw.length === 0) {
432
- filtered.push(rec);
433
- continue;
434
- }
435
- const parsed = new Date(raw);
436
- if (isNaN(parsed.getTime())) {
437
- filtered.push(rec);
438
- continue;
439
- }
440
- if (!latest || parsed > latest)
441
- latest = parsed;
442
- if (!watermarkDate || parsed > watermarkDate)
443
- filtered.push(rec);
444
- }
445
- return { filtered, newWatermark: latest ? latest.toISOString() : null };
446
- }
447
- /** Returns the date field used for incremental filtering, if any. */
448
- watermarkFieldFor(objLower) {
449
- if (objLower === 'activityreport')
450
- return 'completedAt';
451
- if (objLower === 'users')
452
- return 'lastActiveAt';
453
- return null;
454
- }
455
- /**
456
- * Converts a raw Reach 360 record into an ExternalRecord. ActivityReport
457
- * uses a composite {courseId, userId} key.
458
- */
459
- toExternalRecord(raw, objectType) {
460
- const externalID = this.extractExternalID(raw, objectType);
461
- return { ExternalID: externalID, ObjectType: objectType, Fields: raw };
462
- }
463
- extractExternalID(raw, objectType) {
464
- if (objectType.toLowerCase() === 'activityreport') {
465
- const cid = raw['courseId'] != null ? String(raw['courseId']) : '';
466
- const uid = raw['userId'] != null ? String(raw['userId']) : '';
467
- return `${cid}|${uid}`;
468
- }
469
- return raw['id'] != null ? String(raw['id']) : '';
470
- }
471
- // ─── CRUD Operations ─────────────────────────────────────────────
472
- /**
473
- * Supported creates:
474
- * - Invitations → POST /invitations
475
- * - Completions → POST /courses/{courseId}/users/{userId}/completions
476
- * (requires `courseId` + `userId` on Attributes)
477
- * - Group membership → POST /groups/{groupId}/users
478
- * (requires `groupId` on Attributes)
479
- */
480
- async CreateRecord(ctx) {
481
- const companyIntegration = ctx.CompanyIntegration;
482
- const contextUser = ctx.ContextUser;
483
- try {
484
- const auth = await this.Authenticate(companyIntegration, contextUser);
485
- const headers = this.BuildHeaders(auth);
486
- const route = this.resolveCreateRoute(ctx.ObjectName, ctx.Attributes);
487
- if (!route) {
488
- return { Success: false, ErrorMessage: `Reach360 CreateRecord not supported for "${ctx.ObjectName}"`, StatusCode: 400 };
489
- }
490
- const url = `${REACH360_BASE_URL}${route.path}`;
491
- const resp = await this.MakeHTTPRequest(auth, url, 'POST', headers, route.body);
492
- if (resp.Status < 200 || resp.Status >= 300) {
493
- return this.buildCRUDError(resp, 'CreateRecord', ctx.ObjectName);
494
- }
495
- const body = (resp.Body ?? {});
496
- const id = body['id'] != null ? String(body['id']) : (route.fallbackID ?? '');
497
- return this.BuildCreatedResult(id, resp.Status, ctx.ObjectName);
498
- }
499
- catch (err) {
500
- const message = err instanceof Error ? err.message : String(err);
501
- return { Success: false, ErrorMessage: `Reach360 CreateRecord failed: ${message}`, StatusCode: 500 };
502
- }
503
- }
504
- /**
505
- * Resolves which REST route a CreateRecord call maps to. Drives the router
506
- * from object name + payload fields so no hardcoded switch on individual
507
- * object behavior leaks into the HTTP code.
508
- */
509
- resolveCreateRoute(objectName, attrs) {
510
- const obj = objectName.toLowerCase();
511
- if (obj === 'invitations') {
512
- return { path: '/invitations', body: attrs };
513
- }
514
- if (obj === 'completions') {
515
- const courseId = attrs['courseId'];
516
- const userId = attrs['userId'];
517
- if (typeof courseId !== 'string' || typeof userId !== 'string')
518
- return null;
519
- const { courseId: _c, userId: _u, ...body } = attrs;
520
- return {
521
- path: `/courses/${encodeURIComponent(courseId)}/users/${encodeURIComponent(userId)}/completions`,
522
- body,
523
- fallbackID: `${courseId}|${userId}`,
524
- };
525
- }
526
- if (obj === 'groupmembers' || obj === 'groupmembership' || obj === 'groupusers') {
527
- const groupId = attrs['groupId'];
528
- if (typeof groupId !== 'string')
529
- return null;
530
- const { groupId: _g, ...body } = attrs;
531
- return { path: `/groups/${encodeURIComponent(groupId)}/users`, body };
532
- }
533
- return null;
534
- }
535
- /**
536
- * Reach 360 update endpoints (per vendor docs as of audit):
537
- * - Groups → PUT /groups/{groupId} — name, description, tag updates
538
- * - Users → not supported (no PUT/PATCH on /users/{id})
539
- * - Courses → not supported (no PUT/PATCH on /courses/{id})
540
- * - Learning Paths → not supported
541
- * - Reports → not supported
542
- */
543
- async UpdateRecord(ctx) {
544
- const objectLower = ctx.ObjectName.toLowerCase();
545
- if (objectLower === 'groups') {
546
- const companyIntegration = ctx.CompanyIntegration;
547
- const contextUser = ctx.ContextUser;
548
- try {
549
- const auth = await this.Authenticate(companyIntegration, contextUser);
550
- const headers = this.BuildHeaders(auth);
551
- const url = `${REACH360_BASE_URL}/groups/${encodeURIComponent(ctx.ExternalID)}`;
552
- const resp = await this.MakeHTTPRequest(auth, url, 'PUT', headers, ctx.Attributes);
553
- if (resp.Status >= 200 && resp.Status < 300) {
554
- return { Success: true, ExternalID: ctx.ExternalID, StatusCode: resp.Status };
555
- }
556
- return this.buildCRUDError(resp, 'UpdateRecord', ctx.ObjectName);
557
- }
558
- catch (err) {
559
- const message = err instanceof Error ? err.message : String(err);
560
- return { Success: false, ErrorMessage: `Reach360 UpdateRecord failed: ${message}`, StatusCode: 500 };
561
- }
562
- }
563
- return {
564
- Success: false,
565
- ErrorMessage: `Reach360 UpdateRecord not supported for "${ctx.ObjectName}" — only Groups supports PUT in the vendor API.`,
566
- StatusCode: 400,
567
- };
568
- }
569
- /**
570
- * Supported deletes:
571
- * - Users → DELETE /users/{userId}
572
- * - Group membership → DELETE /groups/{groupId}/users/{userId}
573
- * (requires `groupId` to be supplied via ExternalID
574
- * as `{groupId}|{userId}`)
575
- */
576
- async DeleteRecord(ctx) {
577
- const companyIntegration = ctx.CompanyIntegration;
578
- const contextUser = ctx.ContextUser;
579
- try {
580
- const auth = await this.Authenticate(companyIntegration, contextUser);
581
- const headers = this.BuildHeaders(auth);
582
- const path = this.resolveDeletePath(ctx.ObjectName, ctx.ExternalID);
583
- if (!path) {
584
- return { Success: false, ErrorMessage: `Reach360 DeleteRecord not supported for "${ctx.ObjectName}"`, StatusCode: 400 };
585
- }
586
- const url = `${REACH360_BASE_URL}${path}`;
587
- const resp = await this.MakeHTTPRequest(auth, url, 'DELETE', headers);
588
- if (resp.Status < 200 || resp.Status >= 300) {
589
- return this.buildCRUDError(resp, 'DeleteRecord', ctx.ObjectName);
590
- }
591
- return { Success: true, ExternalID: ctx.ExternalID, StatusCode: resp.Status };
592
- }
593
- catch (err) {
594
- const message = err instanceof Error ? err.message : String(err);
595
- return { Success: false, ErrorMessage: `Reach360 DeleteRecord failed: ${message}`, StatusCode: 500 };
596
- }
597
- }
598
- resolveDeletePath(objectName, externalID) {
599
- const obj = objectName.toLowerCase();
600
- if (obj === 'users') {
601
- return `/users/${encodeURIComponent(externalID)}`;
602
- }
603
- if (obj === 'groupmembers' || obj === 'groupmembership' || obj === 'groupusers') {
604
- const [groupId, userId] = externalID.split('|');
605
- if (!groupId || !userId)
606
- return null;
607
- return `/groups/${encodeURIComponent(groupId)}/users/${encodeURIComponent(userId)}`;
608
- }
609
- return null;
610
- }
611
- buildCRUDError(resp, op, objectName) {
612
- const body = resp.Body;
613
- const rawMessage = typeof body?.['message'] === 'string' ? body['message'] : undefined;
614
- return {
615
- Success: false,
616
- ErrorMessage: rawMessage ?? `Reach360 ${op} on ${objectName} failed (HTTP ${resp.Status})`,
617
- StatusCode: resp.Status,
618
- };
619
- }
620
- // ─── Abstract REST hooks (BaseRESTIntegrationConnector) ───────────
621
- async Authenticate(companyIntegration, contextUser) {
622
- const config = await this.parseConfig(companyIntegration, contextUser);
623
- return { Token: config.apiKey, Config: config };
624
- }
625
- BuildHeaders(auth) {
626
- const token = auth.Token ?? '';
627
- return {
628
- 'Authorization': `Bearer ${token}`,
629
- 'Accept': 'application/json',
630
- };
631
- }
632
- /**
633
- * Not used by this connector — FetchChanges walks `nextUrl` directly and
634
- * bypasses the base pagination engine. Still required to satisfy the
635
- * abstract contract.
636
- */
637
- NormalizeResponse(rawBody, responseDataKey) {
638
- if (!responseDataKey)
639
- return Array.isArray(rawBody) ? rawBody : [];
640
- const body = rawBody;
641
- const arr = body[responseDataKey];
642
- return Array.isArray(arr) ? arr : [];
643
- }
644
- /**
645
- * Pagination info is expressed through Reach 360's `nextUrl` pointer which
646
- * the cursor path already consumes directly. This hook is retained for API
647
- * compliance and simply reports "no more pages" to anyone calling through
648
- * the base pipeline.
649
- */
650
- ExtractPaginationInfo(rawBody, _paginationType, _currentPage, _currentOffset, _pageSize) {
651
- const body = rawBody;
652
- const next = typeof body?.nextUrl === 'string' && body.nextUrl.length > 0 ? body.nextUrl : null;
653
- return { HasMore: next != null, NextCursor: next ?? undefined };
654
- }
655
- GetBaseURL(_companyIntegration, _auth) {
656
- return REACH360_BASE_URL;
657
- }
658
- // ─── HTTP transport with retry + throttling ───────────────────────
659
- async MakeHTTPRequest(auth, url, method, headers, body) {
660
- const r360 = auth;
661
- const cfg = r360.Config;
662
- const maxRetries = cfg.MaxRetries ?? DEFAULT_MAX_RETRIES;
663
- const timeoutMs = cfg.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
664
- const minInterval = cfg.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
665
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
666
- await this.throttle(minInterval);
667
- try {
668
- const resp = await this.doFetch(url, method, headers, body, timeoutMs);
669
- this.lastRequestTime = Date.now();
670
- if (resp.Status === 429 || resp.Status === 503) {
671
- if (attempt === maxRetries)
672
- return resp;
673
- await this.sleep(this.backoffMs(attempt));
674
- continue;
675
- }
676
- return resp;
677
- }
678
- catch (err) {
679
- if (attempt === maxRetries)
680
- throw err;
681
- const retryable = this.isRetryableError(err);
682
- if (!retryable)
683
- throw err;
684
- await this.sleep(this.backoffMs(attempt));
685
- }
686
- }
687
- throw new Error(`Reach360 request to ${url} exhausted ${maxRetries + 1} attempts`);
688
- }
689
- /** Executes a single fetch() with an AbortController-backed timeout. */
690
- async doFetch(url, method, headers, body, timeoutMs) {
691
- const controller = new AbortController();
692
- const handle = setTimeout(() => controller.abort(), timeoutMs);
693
- const finalHeaders = { ...headers };
694
- if (body !== undefined && !finalHeaders['Content-Type']) {
695
- finalHeaders['Content-Type'] = 'application/json';
696
- }
697
- try {
698
- const resp = await fetch(url, {
699
- method,
700
- headers: finalHeaders,
701
- body: body !== undefined ? JSON.stringify(body) : undefined,
702
- signal: controller.signal,
703
- });
704
- const respHeaders = {};
705
- resp.headers.forEach((value, key) => { respHeaders[key.toLowerCase()] = value; });
706
- const text = await resp.text();
707
- const parsed = text.length > 0 ? this.safeParseJSON(text) : null;
708
- return { Status: resp.status, Body: parsed, Headers: respHeaders };
709
- }
710
- finally {
711
- clearTimeout(handle);
712
- }
713
- }
714
- safeParseJSON(text) {
715
- try {
716
- return JSON.parse(text);
717
- }
718
- catch {
719
- return text;
720
- }
721
- }
722
- isRetryableError(err) {
723
- const msg = err instanceof Error ? err.message : String(err);
724
- return /abort|timeout|ECONNRESET|ENOTFOUND|ETIMEDOUT|network/i.test(msg);
725
- }
726
- backoffMs(attempt) {
727
- const base = Math.min(1000 * Math.pow(2, attempt), 15000);
728
- const jitter = Math.floor(Math.random() * 250);
729
- return base + jitter;
730
- }
731
- async throttle(minIntervalMs) {
732
- const elapsed = Date.now() - this.lastRequestTime;
733
- if (elapsed < minIntervalMs) {
734
- await this.sleep(minIntervalMs - elapsed);
735
- }
736
- }
737
- sleep(ms) {
738
- return new Promise(resolve => setTimeout(resolve, ms));
739
- }
740
- // ─── Cursor helpers ──────────────────────────────────────────────
741
- /** Encodes a full `nextUrl` into an opaque cursor string. */
742
- EncodeCursor(nextUrl) {
743
- return Buffer.from(nextUrl, 'utf-8').toString('base64url');
744
- }
745
- /** Decodes a previously encoded cursor back to its `nextUrl`. */
746
- DecodeCursor(cursor) {
747
- return Buffer.from(cursor, 'base64url').toString('utf-8');
748
- }
749
- // ─── Config parsing ───────────────────────────────────────────────
750
- async parseConfig(companyIntegration, contextUser) {
751
- const credentialID = companyIntegration.CredentialID;
752
- if (credentialID) {
753
- const fromCred = await this.loadFromCredential(credentialID, contextUser);
754
- if (fromCred)
755
- return fromCred;
756
- }
757
- const raw = companyIntegration.Configuration;
758
- if (!raw) {
759
- throw new Error('Reach360Connector: No credential or Configuration JSON found on CompanyIntegration');
760
- }
761
- const parsed = JSON.parse(raw);
762
- return this.validateConfig(parsed);
763
- }
764
- async loadFromCredential(credentialID, contextUser, provider) {
765
- const md = provider ?? new Metadata();
766
- const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
767
- const loaded = await credential.Load(credentialID);
768
- if (!loaded || !credential.Values)
769
- return null;
770
- try {
771
- const parsed = JSON.parse(credential.Values);
772
- return this.validateConfig(parsed);
773
- }
774
- catch {
775
- return null;
776
- }
777
- }
778
- validateConfig(raw) {
779
- if (!raw.apiKey || typeof raw.apiKey !== 'string') {
780
- throw new Error('Reach360Connector: apiKey is required');
781
- }
782
- return {
783
- apiKey: raw.apiKey,
784
- MaxRetries: raw.MaxRetries ?? DEFAULT_MAX_RETRIES,
785
- RequestTimeoutMs: raw.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
786
- MinRequestIntervalMs: raw.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
787
- };
788
- }
789
- };
790
- Reach360Connector = __decorate([
791
- RegisterClass(BaseIntegrationConnector, 'Reach360Connector')
792
- ], Reach360Connector);
793
- export { Reach360Connector };
794
- /** Tree-shaking prevention function — import and call from module entry point. */
795
- export function LoadReach360Connector() { }
796
- //# sourceMappingURL=Reach360Connector.js.map