@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,893 +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 { IntegrationEngineBase } from '@memberjunction/integration-engine-base';
10
- import { BaseIntegrationConnector, } from '@memberjunction/integration-engine';
11
- // ─── Constants ───────────────────────────────────────────────────────
12
- /** Default HTTP timeout in ms. */
13
- const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
14
- /** Default minimum interval between requests in ms. */
15
- const DEFAULT_MIN_REQUEST_INTERVAL_MS = 250;
16
- /** Maximum retries on 429/503 / GraphQL-level errors before giving up. */
17
- const MAX_RETRIES = 4;
18
- /** Base backoff (ms) for exponential backoff on 429/503. */
19
- const BASE_BACKOFF_MS = 1000;
20
- /** Maximum cap on the backoff delay. */
21
- const MAX_BACKOFF_MS = 30000;
22
- /** Fields on a remote MJ entity that, when present, suggest conversational shape. */
23
- const CONVERSATIONAL_MARKER_FIELDS = new Set([
24
- 'conversationid',
25
- 'message',
26
- 'role',
27
- ]);
28
- /** Static seed entities — merged with runtime introspection in DiscoverObjects. */
29
- const MJ_TO_MJ_STATIC_OBJECTS = [
30
- {
31
- Name: 'Conversations',
32
- Label: 'Conversations',
33
- Description: 'Remote MJ Conversations — maps to MJ: Conversations.',
34
- SupportsIncrementalSync: true,
35
- SupportsWrite: false,
36
- },
37
- {
38
- Name: 'ConversationDetails',
39
- Label: 'Conversation Details',
40
- Description: 'Remote MJ Conversation Details — maps to MJ: Conversation Details.',
41
- SupportsIncrementalSync: true,
42
- SupportsWrite: false,
43
- },
44
- {
45
- Name: 'AIAgents',
46
- Label: 'AI Agents',
47
- Description: 'Remote MJ AI Agents — reference data for agent definitions.',
48
- SupportsIncrementalSync: true,
49
- SupportsWrite: false,
50
- },
51
- {
52
- Name: 'AIAgentRuns',
53
- Label: 'AI Agent Runs',
54
- Description: 'Remote MJ AI Agent Runs — execution logs (GraphQL: MJ_AIAgentRuns).',
55
- SupportsIncrementalSync: true,
56
- SupportsWrite: false,
57
- },
58
- {
59
- Name: 'AIAgentRunSteps',
60
- Label: 'AI Agent Run Steps',
61
- Description: 'Remote MJ AI Agent Run Steps — step-level run detail (GraphQL: MJ_AIAgentRunSteps).',
62
- SupportsIncrementalSync: true,
63
- SupportsWrite: false,
64
- },
65
- {
66
- Name: 'Users',
67
- Label: 'Users',
68
- Description: 'Remote MJ Users — reference data for conversation ownership.',
69
- SupportsIncrementalSync: true,
70
- SupportsWrite: false,
71
- },
72
- ];
73
- // ─── Connector Implementation ────────────────────────────────────────
74
- /**
75
- * Read-only connector that pulls conversational data (Conversations,
76
- * Conversation Details, optional AI Agent Runs/Steps, reference Users and
77
- * Agents) from a remote, self-hosted MemberJunction GraphQL API into
78
- * this MJ's native conversation entities.
79
- *
80
- * Destination mapping on this MJ:
81
- * - Conversations -> `MJ: Conversations`
82
- * - ConversationDetails -> `MJ: Conversation Details`
83
- * - AIAgents / Users / AIAgentRuns / AIAgentRunSteps are reference-only by
84
- * default and not auto-mapped to MJ entities.
85
- *
86
- * Design notes:
87
- * - Extends `BaseIntegrationConnector` directly (not REST) because MJ
88
- * uses GraphQL, not REST-over-HTTP-verbs.
89
- * - No dependency on `@memberjunction/graphql-data-provider` — we POST
90
- * plain GraphQL over `fetch` to keep the dependency surface minimal.
91
- * - The GraphQL query is **built from IntegrationObject metadata** — the
92
- * connector does NOT switch on entity names. `DefaultQueryParams`
93
- * carries the `graphql_fields` selection, `watermark_field`, and
94
- * `mj_target_entity` / `mj_field_map` hints.
95
- * - MJ's GraphQL filter convention is `ExtraFilter` — a SQL-like WHERE
96
- * clause. We build the filter string from the watermark, escaping the
97
- * datetime literal, and send it as a GraphQL **variable**
98
- * (`$extraFilter: String`) rather than interpolating into the query
99
- * text. This avoids injection from the watermark value.
100
- * - Pagination is offset/limit via MJ's `skip` / `top` args.
101
- * - 200 OK with non-empty `errors[]` is treated as a transient failure
102
- * and retried like a 5xx.
103
- *
104
- * Configuration shape:
105
- * ```json
106
- * {
107
- * "GraphQLEndpoint": "https://mj.customer.org/graphql",
108
- * "ApiKey": "<read-only key>",
109
- * "AuthHeader": "x-api-key: {key}",
110
- * "RequestTimeoutMs": 30000,
111
- * "MinRequestIntervalMs": 250
112
- * }
113
- * ```
114
- */
115
- let MJToMJConnector = class MJToMJConnector extends BaseIntegrationConnector {
116
- constructor() {
117
- // ── Instance state ───────────────────────────────────────────────
118
- super(...arguments);
119
- /** Resolved config for the current run (populated on first auth). */
120
- this.resolvedConfig = null;
121
- /** Timestamp of the last HTTP request for throttling. */
122
- this.lastRequestTime = 0;
123
- }
124
- // ── Capability getters (all default to false — read-only) ────────
125
- get SupportsCreate() { return false; }
126
- get SupportsUpdate() { return false; }
127
- get SupportsDelete() { return false; }
128
- get SupportsSearch() { return false; }
129
- get SupportsListing() { return false; }
130
- get IntegrationName() { return 'MJ to MJ'; }
131
- // ── Read-only guards ─────────────────────────────────────────────
132
- async CreateRecord() {
133
- throw new Error('MJToMJ connector is read-only — CreateRecord is not supported.');
134
- }
135
- async UpdateRecord() {
136
- throw new Error('MJToMJ connector is read-only — UpdateRecord is not supported.');
137
- }
138
- async DeleteRecord() {
139
- throw new Error('MJToMJ connector is read-only — DeleteRecord is not supported.');
140
- }
141
- // ─── TestConnection ──────────────────────────────────────────────
142
- /**
143
- * Validates connectivity by running a minimal GraphQL introspection
144
- * query. Any 200 response with a populated `__schema.queryType.name`
145
- * is treated as success.
146
- *
147
- * - 401 => "API key rejected"
148
- * - 403 => "API key lacks schema access"
149
- * - other non-2xx => surfaced with the HTTP status for debugging
150
- */
151
- async TestConnection(companyIntegration, contextUser) {
152
- try {
153
- const config = await this.resolveConfig(companyIntegration, contextUser);
154
- const result = await this.probeSchema(config);
155
- return result;
156
- }
157
- catch (err) {
158
- const message = err instanceof Error ? err.message : String(err);
159
- return {
160
- Success: false,
161
- Message: `MJToMJ connection test failed: ${message}. ` +
162
- `Verify GraphQLEndpoint, ApiKey, and AuthHeader style with the remote MJ admin.`,
163
- };
164
- }
165
- }
166
- /**
167
- * Runs the minimal `__schema.queryType.name` introspection query and
168
- * translates HTTP-level outcomes into a user-facing connection result.
169
- */
170
- async probeSchema(config) {
171
- const query = 'query { __schema { queryType { name } } }';
172
- const response = await this.postGraphQL(config, query, undefined);
173
- if (response.HTTPStatus === 401) {
174
- return { Success: false, Message: 'Remote MJ rejected the API key (HTTP 401). Re-check the key value and header style.' };
175
- }
176
- if (response.HTTPStatus === 403) {
177
- return { Success: false, Message: 'Remote MJ accepted the key but the key lacks schema-introspection access (HTTP 403).' };
178
- }
179
- if (response.HTTPStatus < 200 || response.HTTPStatus >= 300) {
180
- return { Success: false, Message: `Remote MJ returned HTTP ${response.HTTPStatus} during introspection.` };
181
- }
182
- if (response.Body.errors && response.Body.errors.length > 0) {
183
- const joined = response.Body.errors.map(e => e.message).join('; ');
184
- return { Success: false, Message: `GraphQL errors during introspection: ${joined}` };
185
- }
186
- const queryTypeName = response.Body.data?.__schema?.queryType?.name;
187
- if (!queryTypeName) {
188
- return { Success: false, Message: 'Remote MJ responded but __schema.queryType was empty — introspection may be disabled.' };
189
- }
190
- return {
191
- Success: true,
192
- Message: `Connected to remote MemberJunction GraphQL (queryType: ${queryTypeName}). Ready to pull conversational data.`,
193
- ServerVersion: `MemberJunction GraphQL (${queryTypeName})`,
194
- };
195
- }
196
- // ─── DiscoverObjects ─────────────────────────────────────────────
197
- /**
198
- * Merges static seed objects with entities the connector detects via
199
- * `__schema.queryType.fields` whose names look conversational. Anything
200
- * discovered dynamically is flagged `IsCustom: true` through a
201
- * "Custom:" description prefix.
202
- */
203
- async DiscoverObjects(companyIntegration, contextUser) {
204
- const staticObjects = MJ_TO_MJ_STATIC_OBJECTS.map(o => ({ ...o }));
205
- const staticNames = this.toLowerSet(staticObjects.map(o => o.Name));
206
- try {
207
- const config = await this.resolveConfig(companyIntegration, contextUser);
208
- const schemaFields = await this.fetchRootQueryFields(config);
209
- const discovered = await this.findCustomConversationalObjects(config, schemaFields, staticNames);
210
- return [...staticObjects, ...discovered];
211
- }
212
- catch (err) {
213
- const message = err instanceof Error ? err.message : String(err);
214
- console.warn(`[MJToMJ] DiscoverObjects runtime introspection failed (${message}) — returning static objects only.`);
215
- return staticObjects;
216
- }
217
- }
218
- /** Fetches the list of queryable root fields from the remote schema. */
219
- async fetchRootQueryFields(config) {
220
- const query = `query {
221
- __schema {
222
- queryType {
223
- name
224
- fields {
225
- name
226
- description
227
- type { kind name ofType { kind name ofType { kind name } } }
228
- }
229
- }
230
- }
231
- }`;
232
- const response = await this.postGraphQL(config, query, undefined);
233
- this.assertGraphQLOk(response, 'introspect root schema');
234
- return response.Body.data?.__schema?.queryType?.fields ?? [];
235
- }
236
- /**
237
- * Filters root query fields to those that resemble conversational
238
- * entities by probing each candidate's type with an introspection call
239
- * and checking for marker fields (ConversationID / Message / Role).
240
- */
241
- async findCustomConversationalObjects(config, rootFields, staticNames) {
242
- const discovered = [];
243
- const seen = new Set(staticNames);
244
- for (const rootField of rootFields) {
245
- const normalizedName = rootField.name;
246
- const lc = normalizedName.toLowerCase();
247
- if (seen.has(lc))
248
- continue;
249
- const elementTypeName = this.extractListElementTypeName(rootField.type);
250
- if (!elementTypeName)
251
- continue;
252
- const isConversational = await this.typeHasConversationalShape(config, elementTypeName);
253
- if (!isConversational)
254
- continue;
255
- seen.add(lc);
256
- discovered.push({
257
- Name: normalizedName,
258
- Label: normalizedName,
259
- Description: `Custom: discovered via GraphQL introspection (type ${elementTypeName}).`,
260
- SupportsIncrementalSync: true,
261
- SupportsWrite: false,
262
- });
263
- }
264
- return discovered;
265
- }
266
- /** Returns true if the given GraphQL type has any conversational marker fields. */
267
- async typeHasConversationalShape(config, typeName) {
268
- const introspected = await this.introspectType(config, typeName);
269
- if (!introspected?.fields)
270
- return false;
271
- return introspected.fields.some(f => CONVERSATIONAL_MARKER_FIELDS.has(f.name.toLowerCase()));
272
- }
273
- // ─── DiscoverFields ──────────────────────────────────────────────
274
- /**
275
- * Merges statically-configured fields (from IntegrationObjectField) with
276
- * fields discovered via GraphQL introspection on the corresponding
277
- * GraphQL type. Anything seen only at runtime is flagged `IsCustom`.
278
- */
279
- async DiscoverFields(companyIntegration, objectName, contextUser) {
280
- const staticFields = this.getStaticFieldSchemas(companyIntegration.IntegrationID, objectName);
281
- const staticNames = this.toLowerSet(staticFields.map(f => f.Name));
282
- try {
283
- const config = await this.resolveConfig(companyIntegration, contextUser);
284
- const typeName = this.resolveGraphQLTypeName(companyIntegration.IntegrationID, objectName);
285
- if (!typeName)
286
- return staticFields;
287
- const introspected = await this.introspectType(config, typeName);
288
- if (!introspected?.fields)
289
- return staticFields;
290
- const discovered = this.buildCustomFieldSchemas(introspected.fields, staticNames);
291
- return [...staticFields, ...discovered];
292
- }
293
- catch (err) {
294
- const message = err instanceof Error ? err.message : String(err);
295
- console.warn(`[MJToMJ] DiscoverFields for "${objectName}" failed (${message}) — returning static fields only.`);
296
- return staticFields;
297
- }
298
- }
299
- /**
300
- * Resolves the expected remote GraphQL type name for an IntegrationObject
301
- * by looking up the type of the list query field named by `APIPath`.
302
- */
303
- resolveGraphQLTypeName(integrationID, objectName) {
304
- const obj = this.tryGetCachedObject(integrationID, objectName);
305
- if (!obj)
306
- return null;
307
- // APIPath stores the GraphQL query name (e.g., "Conversations").
308
- // Without a live root-schema cache, we cannot resolve the exact type
309
- // name here; callers that need the element type should use
310
- // extractListElementTypeName() on introspection results instead.
311
- // Fall back to the object name itself, which matches MJ conventions
312
- // where the query `Foo` returns a list of `Foo`.
313
- return this.stripMJPrefix(obj.APIPath) || objectName;
314
- }
315
- /** Builds ExternalFieldSchema entries for introspected fields absent from static metadata. */
316
- buildCustomFieldSchemas(introspectedFields, staticNames) {
317
- const out = [];
318
- for (const f of introspectedFields) {
319
- if (staticNames.has(f.name.toLowerCase()))
320
- continue;
321
- out.push({
322
- Name: f.name,
323
- Label: f.name,
324
- Description: `Custom: ${f.description ?? 'discovered via GraphQL introspection'}`,
325
- DataType: this.mapIntrospectionTypeToMJType(f.type),
326
- IsRequired: this.isNonNull(f.type),
327
- IsUniqueKey: f.name.toLowerCase() === 'id',
328
- IsReadOnly: true,
329
- IsForeignKey: false,
330
- ForeignKeyTarget: null,
331
- });
332
- }
333
- return out;
334
- }
335
- // ─── FetchChanges ────────────────────────────────────────────────
336
- /**
337
- * Pulls a batch of records from the remote MJ using a GraphQL query
338
- * built entirely from `DefaultQueryParams` on the IntegrationObject:
339
- *
340
- * - `graphql_fields` → GraphQL field selection
341
- * - `watermark_field` → used to build an ExtraFilter
342
- * - pagination via `skip` / `top` (offset / limit)
343
- *
344
- * Watermark values are passed as GraphQL variables, never string-concatenated
345
- * into the query body.
346
- */
347
- async FetchChanges(ctx) {
348
- const obj = this.getCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
349
- const fields = this.getCachedFields(obj.ID);
350
- const hints = this.parseObjectHints(obj);
351
- const config = await this.resolveConfig(ctx.CompanyIntegration, ctx.ContextUser);
352
- const offset = ctx.CurrentOffset ?? 0;
353
- const limit = this.computeBatchSize(obj, ctx.BatchSize);
354
- const filter = this.buildExtraFilter(hints.WatermarkField, ctx.WatermarkValue);
355
- const requestedFields = ctx.RequestedSourceFields;
356
- const selection = this.narrowSelection(hints.GraphQLFields, requestedFields);
357
- const { query, variables } = this.buildListQuery(hints.GraphQLQueryName, selection, offset, limit, filter);
358
- const response = await this.postGraphQL(config, query, variables);
359
- this.assertGraphQLOk(response, `fetch ${ctx.ObjectName}`);
360
- const rows = this.extractRows(response.Body.data, hints.ResponseDataKey);
361
- const pkFieldNames = this.findPrimaryKeyFieldNames(fields);
362
- const records = rows.map(r => this.toExternalRecord(r, ctx.ObjectName, pkFieldNames));
363
- const newWatermark = this.computeNewWatermark(rows, hints.WatermarkField, ctx.WatermarkValue);
364
- return {
365
- Records: records,
366
- HasMore: rows.length >= limit,
367
- NextOffset: offset + rows.length,
368
- NewWatermarkValue: newWatermark ?? undefined,
369
- };
370
- }
371
- /** Extracts the list of rows from the GraphQL data envelope under the configured key. */
372
- extractRows(data, key) {
373
- if (!data || typeof data !== 'object')
374
- return [];
375
- const container = data;
376
- const value = container[key];
377
- return Array.isArray(value) ? value.filter((r) => r != null && typeof r === 'object') : [];
378
- }
379
- /** Returns the max row count to request in a single page. */
380
- computeBatchSize(obj, batchSize) {
381
- const defaultPageSize = obj.DefaultPageSize ?? 100;
382
- if (batchSize && batchSize > 0)
383
- return Math.min(batchSize, defaultPageSize);
384
- return defaultPageSize;
385
- }
386
- /**
387
- * Builds an MJ `ExtraFilter` clause from the watermark field and value.
388
- * Returns null when either piece is missing, which signals "no filter".
389
- * Datetime literals are wrapped in single quotes and double-single-quoted
390
- * to guard against injection from exotic watermark values.
391
- */
392
- buildExtraFilter(watermarkField, watermarkValue) {
393
- if (!watermarkField || !watermarkValue)
394
- return null;
395
- const safeValue = String(watermarkValue).replace(/'/g, "''");
396
- return `${watermarkField} > '${safeValue}'`;
397
- }
398
- /**
399
- * Returns the list of GraphQL fields to request. If the engine supplied
400
- * RequestedSourceFields, intersect with the configured `graphql_fields`;
401
- * always include primary-key-like fields to preserve ExternalID derivation.
402
- */
403
- narrowSelection(configuredFields, requestedFields) {
404
- if (!requestedFields || requestedFields.length === 0)
405
- return configuredFields;
406
- const requestedLower = this.toLowerSet(requestedFields);
407
- const preserved = configuredFields.filter(f => requestedLower.has(f.toLowerCase()) || f.toLowerCase() === 'id');
408
- return preserved.length > 0 ? preserved : configuredFields;
409
- }
410
- /**
411
- * Builds a paginated list query of the shape
412
- * `query Fetch($skip:Int,$top:Int,$extraFilter:String) { <name>(skip:$skip,top:$top,ExtraFilter:$extraFilter) { field1 field2 } }`.
413
- */
414
- buildListQuery(queryName, fieldSelection, offset, limit, filter) {
415
- const sanitizedName = this.sanitizeGraphQLName(queryName);
416
- const selectionText = this.escapeFieldSelection(fieldSelection);
417
- const args = ['skip: $skip', 'top: $top'];
418
- if (filter != null) {
419
- args.push('ExtraFilter: $extraFilter');
420
- }
421
- const varDefs = ['$skip: Int!', '$top: Int!'];
422
- if (filter != null)
423
- varDefs.push('$extraFilter: String');
424
- const query = `query Fetch(${varDefs.join(', ')}) {
425
- ${sanitizedName}(${args.join(', ')}) {
426
- ${selectionText}
427
- }
428
- }`;
429
- const variables = { skip: offset, top: limit };
430
- if (filter != null)
431
- variables.extraFilter = filter;
432
- return { query, variables };
433
- }
434
- // ─── DefaultQueryParams parsing ──────────────────────────────────
435
- /**
436
- * Extracts the GraphQL hint block from an IntegrationObject's
437
- * DefaultQueryParams. Uses safe fallbacks so an object with partial
438
- * hints can still fetch at least its declared primary key.
439
- */
440
- parseObjectHints(obj) {
441
- const parsed = this.safeParseJson(obj.DefaultQueryParams);
442
- const graphQLFields = this.extractFieldListFromHints(parsed, obj.ID);
443
- const watermarkField = this.pickString(parsed, 'watermark_field') ?? null;
444
- const graphQLQueryName = obj.APIPath?.trim() || obj.Name;
445
- const responseDataKey = obj.ResponseDataKey?.trim() || graphQLQueryName;
446
- return { GraphQLFields: graphQLFields, WatermarkField: watermarkField, GraphQLQueryName: graphQLQueryName, ResponseDataKey: responseDataKey };
447
- }
448
- /**
449
- * Resolves the declared GraphQL field selection for an object. Prefers
450
- * `graphql_fields` on DefaultQueryParams; falls back to the IntegrationObjectField
451
- * metadata when not set (ensures at least the PK is requested).
452
- */
453
- extractFieldListFromHints(parsed, objectID) {
454
- const raw = parsed ? this.pickString(parsed, 'graphql_fields') : undefined;
455
- if (raw) {
456
- return raw.split(',').map(s => s.trim()).filter(s => s.length > 0);
457
- }
458
- // Fallback: derive from IntegrationObjectField list.
459
- const fields = this.getCachedFields(objectID);
460
- if (fields.length === 0)
461
- return ['ID'];
462
- return fields.filter(f => f.Status === 'Active').map(f => f.Name);
463
- }
464
- // ─── GraphQL name / selection sanitization ───────────────────────
465
- /**
466
- * Sanitizes a GraphQL identifier — strips anything outside `[A-Za-z0-9_]`.
467
- * Names that start with a digit are prefixed with `_` to stay valid.
468
- */
469
- sanitizeGraphQLName(name) {
470
- const trimmed = name.trim();
471
- if (!trimmed)
472
- throw new Error('MJToMJConnector: GraphQL query name cannot be empty.');
473
- const cleaned = trimmed.replace(/[^A-Za-z0-9_]/g, '');
474
- if (!cleaned)
475
- throw new Error(`MJToMJConnector: GraphQL query name "${name}" contains no valid characters.`);
476
- return /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
477
- }
478
- /** Sanitizes each requested field name so the built query stays valid GraphQL. */
479
- escapeFieldSelection(fields) {
480
- const safe = fields
481
- .map(f => this.sanitizeGraphQLName(f))
482
- .filter(f => f.length > 0);
483
- return safe.length > 0 ? safe.join('\n ') : 'ID';
484
- }
485
- // ─── GraphQL transport ───────────────────────────────────────────
486
- /**
487
- * Executes a single GraphQL POST. Automatically retries on 429/503 and
488
- * on GraphQL-level transient errors (200 with non-empty `errors[]`).
489
- */
490
- async postGraphQL(config, query, variables) {
491
- const headers = this.buildHeaders(config);
492
- const bodyPayload = JSON.stringify(variables ? { query, variables } : { query });
493
- await this.applyThrottle(config);
494
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
495
- const response = await this.fetchRaw(config, headers, bodyPayload);
496
- const status = response.status;
497
- if (status === 429 || status === 503) {
498
- if (attempt === MAX_RETRIES) {
499
- return { HTTPStatus: status, Body: await this.parseGraphQLBody(response) };
500
- }
501
- await this.sleep(this.computeBackoffMs(response, attempt));
502
- continue;
503
- }
504
- this.lastRequestTime = Date.now();
505
- const body = await this.parseGraphQLBody(response);
506
- if (status >= 200 && status < 300 && this.shouldRetryGraphQLErrors(body) && attempt < MAX_RETRIES) {
507
- await this.sleep(this.computeBackoffMs(response, attempt));
508
- continue;
509
- }
510
- return { HTTPStatus: status, Body: body };
511
- }
512
- throw new Error('MJToMJConnector: GraphQL request failed after maximum retries.');
513
- }
514
- /** Executes the raw fetch with an AbortController-based timeout. */
515
- async fetchRaw(config, headers, bodyPayload) {
516
- const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
517
- const controller = new AbortController();
518
- const timer = setTimeout(() => controller.abort(), timeoutMs);
519
- try {
520
- return await fetch(config.GraphQLEndpoint, {
521
- method: 'POST',
522
- headers,
523
- body: bodyPayload,
524
- signal: controller.signal,
525
- });
526
- }
527
- catch (err) {
528
- if (err instanceof Error && err.name === 'AbortError') {
529
- throw new Error(`MJToMJ GraphQL request timed out after ${timeoutMs / 1000}s.`);
530
- }
531
- throw err;
532
- }
533
- finally {
534
- clearTimeout(timer);
535
- }
536
- }
537
- /** Parses a fetch Response body into the GraphQL envelope. */
538
- async parseGraphQLBody(response) {
539
- try {
540
- const text = await response.text();
541
- if (!text)
542
- return {};
543
- return JSON.parse(text);
544
- }
545
- catch {
546
- return {};
547
- }
548
- }
549
- /** Returns true when the response body carries recoverable-looking GraphQL errors. */
550
- shouldRetryGraphQLErrors(body) {
551
- if (!body.errors || body.errors.length === 0)
552
- return false;
553
- const retryableKeywords = ['timeout', 'temporarily', 'try again', 'rate limit', 'busy', 'unavailable'];
554
- return body.errors.some(e => {
555
- const msg = (e.message ?? '').toLowerCase();
556
- return retryableKeywords.some(k => msg.includes(k));
557
- });
558
- }
559
- /**
560
- * Asserts that a GraphQL response was successful — throws a descriptive
561
- * error otherwise. Used on introspection / schema-level calls where we
562
- * can't return a partial result.
563
- */
564
- assertGraphQLOk(response, operation) {
565
- if (response.HTTPStatus < 200 || response.HTTPStatus >= 300) {
566
- throw new Error(`MJToMJConnector: HTTP ${response.HTTPStatus} while trying to ${operation}.`);
567
- }
568
- if (response.Body.errors && response.Body.errors.length > 0) {
569
- const joined = response.Body.errors.map(e => e.message).join('; ');
570
- throw new Error(`MJToMJConnector: GraphQL errors while trying to ${operation} — ${joined}`);
571
- }
572
- }
573
- /** Builds HTTP headers honoring the configured auth-header style. */
574
- buildHeaders(config) {
575
- const headers = {
576
- 'Content-Type': 'application/json',
577
- 'Accept': 'application/json',
578
- };
579
- const style = config.AuthHeader ?? 'x-api-key: {key}';
580
- if (style === 'Authorization: Bearer {key}') {
581
- headers['Authorization'] = `Bearer ${config.ApiKey}`;
582
- }
583
- else {
584
- headers['x-api-key'] = config.ApiKey;
585
- }
586
- return headers;
587
- }
588
- /** Enforces MinRequestIntervalMs between consecutive calls. */
589
- async applyThrottle(config) {
590
- const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
591
- const elapsed = Date.now() - this.lastRequestTime;
592
- if (elapsed < minInterval) {
593
- await this.sleep(minInterval - elapsed);
594
- }
595
- }
596
- /** Returns a Promise that resolves after `ms` milliseconds. */
597
- sleep(ms) {
598
- return new Promise(resolve => setTimeout(resolve, ms));
599
- }
600
- /** Honors Retry-After if present, otherwise exponential backoff capped at MAX_BACKOFF_MS. */
601
- computeBackoffMs(response, attempt) {
602
- const retryAfter = response.headers.get('retry-after');
603
- if (retryAfter) {
604
- const seconds = Number(retryAfter);
605
- if (!Number.isNaN(seconds) && seconds > 0) {
606
- return Math.min(seconds * 1000, MAX_BACKOFF_MS);
607
- }
608
- }
609
- return Math.min(BASE_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
610
- }
611
- // ─── GraphQL introspection helpers ───────────────────────────────
612
- /** Runs a `__type(name: $name)` introspection query on a single type. */
613
- async introspectType(config, typeName) {
614
- const query = `query IntrospectType($typeName: String!) {
615
- __type(name: $typeName) {
616
- name
617
- kind
618
- fields { name description type { kind name ofType { kind name ofType { kind name } } } }
619
- }
620
- }`;
621
- const response = await this.postGraphQL(config, query, { typeName });
622
- if (response.HTTPStatus < 200 || response.HTTPStatus >= 300)
623
- return null;
624
- if (response.Body.errors && response.Body.errors.length > 0)
625
- return null;
626
- return response.Body.data?.__type ?? null;
627
- }
628
- /**
629
- * Walks a GraphQL type reference and returns the innermost NAMED type
630
- * that sits underneath any LIST/NON_NULL wrappers — this is the element
631
- * type for a list-returning query like `Conversations: [Conversation!]!`.
632
- */
633
- extractListElementTypeName(type) {
634
- let current = type;
635
- while (current) {
636
- if (current.kind === 'LIST' && current.ofType) {
637
- return this.extractInnerNamedType(current.ofType);
638
- }
639
- current = current.ofType ?? null;
640
- }
641
- return null;
642
- }
643
- /** Returns the innermost NAMED type ignoring NON_NULL wrappers. */
644
- extractInnerNamedType(type) {
645
- let current = type;
646
- while (current) {
647
- if (current.kind === 'NON_NULL' || current.kind === 'LIST') {
648
- current = current.ofType ?? null;
649
- continue;
650
- }
651
- return current.name ?? null;
652
- }
653
- return null;
654
- }
655
- /** Returns true if the OUTERMOST type is NON_NULL. */
656
- isNonNull(type) {
657
- return type.kind === 'NON_NULL';
658
- }
659
- /**
660
- * Maps a GraphQL scalar/type reference to the coarse MJ data-type label
661
- * used by `ExternalFieldSchema.DataType`. Leans conservative — unknowns
662
- * fall back to `nvarchar`.
663
- */
664
- mapIntrospectionTypeToMJType(type) {
665
- const named = this.extractInnerNamedType(type);
666
- if (!named)
667
- return 'nvarchar';
668
- switch (named) {
669
- case 'Int':
670
- return 'int';
671
- case 'Float':
672
- return 'decimal';
673
- case 'Boolean':
674
- return 'bit';
675
- case 'ID':
676
- case 'String':
677
- return 'nvarchar';
678
- case 'Date':
679
- case 'DateTime':
680
- case 'DateTimeOffset':
681
- return 'datetimeoffset';
682
- default:
683
- return 'nvarchar';
684
- }
685
- }
686
- // ─── Config resolution ───────────────────────────────────────────
687
- /**
688
- * Resolves a MJToMJConnectionConfig from either the CompanyIntegration's
689
- * linked credential record or its legacy Configuration JSON field.
690
- * Result is cached on the instance so subsequent calls skip the lookup.
691
- */
692
- async resolveConfig(companyIntegration, contextUser) {
693
- if (this.resolvedConfig)
694
- return this.resolvedConfig;
695
- const credentialID = companyIntegration.CredentialID;
696
- if (credentialID) {
697
- const fromCred = await this.loadConfigFromCredential(credentialID, contextUser);
698
- if (fromCred) {
699
- this.resolvedConfig = fromCred;
700
- return fromCred;
701
- }
702
- }
703
- const configJson = companyIntegration.Configuration;
704
- if (configJson) {
705
- const parsed = this.parseConfigJson(configJson);
706
- this.resolvedConfig = parsed;
707
- return parsed;
708
- }
709
- throw new Error('MJToMJConnector: No credentials or configuration found. ' +
710
- 'Attach a "MJ API Key" credential with GraphQLEndpoint + ApiKey or set Configuration JSON on the CompanyIntegration.');
711
- }
712
- /** Loads config from a `MJ: Credentials` record; returns null on miss/parse failure. */
713
- async loadConfigFromCredential(credentialID, contextUser, provider) {
714
- const md = provider ?? new Metadata();
715
- const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
716
- const loaded = await credential.Load(credentialID);
717
- if (!loaded || !credential.Values)
718
- return null;
719
- try {
720
- return this.parseConfigJson(credential.Values);
721
- }
722
- catch {
723
- return null;
724
- }
725
- }
726
- /**
727
- * Parses a JSON config payload into a fully-typed MJToMJConnectionConfig.
728
- * Accepts PascalCase and camelCase key spellings so either the JSON schema's
729
- * default camelCase or hand-written PascalCase works interchangeably.
730
- */
731
- parseConfigJson(json) {
732
- const parsed = this.safeParseJson(json);
733
- if (!parsed)
734
- throw new Error('MJToMJConnector: Configuration JSON is malformed.');
735
- const endpoint = this.pickString(parsed, 'GraphQLEndpoint', 'graphqlEndpoint', 'graphQLEndpoint', 'Endpoint', 'endpoint');
736
- const apiKey = this.pickString(parsed, 'ApiKey', 'apiKey', 'APIKey', 'api_key');
737
- if (!endpoint || !apiKey) {
738
- throw new Error('MJToMJConnector: Configuration must include GraphQLEndpoint and ApiKey.');
739
- }
740
- const authHeader = this.normalizeAuthHeader(this.pickString(parsed, 'AuthHeader', 'authHeader'));
741
- return {
742
- GraphQLEndpoint: this.stripTrailingSlash(endpoint),
743
- ApiKey: apiKey,
744
- AuthHeader: authHeader,
745
- RequestTimeoutMs: this.pickPositiveInt(parsed, 'RequestTimeoutMs', 'requestTimeoutMs'),
746
- MinRequestIntervalMs: this.pickPositiveInt(parsed, 'MinRequestIntervalMs', 'minRequestIntervalMs'),
747
- };
748
- }
749
- /** Normalizes a raw auth header string into the typed union. */
750
- normalizeAuthHeader(raw) {
751
- if (!raw)
752
- return 'x-api-key: {key}';
753
- const trimmed = raw.trim();
754
- if (trimmed === 'Authorization: Bearer {key}' || /^authorization\b/i.test(trimmed)) {
755
- return 'Authorization: Bearer {key}';
756
- }
757
- return 'x-api-key: {key}';
758
- }
759
- // ─── IntegrationObject/Field metadata accessors ──────────────────
760
- /** Returns the cached IntegrationObject entity or throws if not present. */
761
- getCachedObject(integrationID, objectName) {
762
- const obj = IntegrationEngineBase.Instance.GetIntegrationObject(integrationID, objectName);
763
- if (!obj) {
764
- throw new Error(`MJToMJConnector: IntegrationObject not found for "${objectName}" (integration ${integrationID}).`);
765
- }
766
- return obj;
767
- }
768
- /** Null-safe version of getCachedObject — used in DiscoverFields. */
769
- tryGetCachedObject(integrationID, objectName) {
770
- try {
771
- return this.getCachedObject(integrationID, objectName);
772
- }
773
- catch {
774
- return null;
775
- }
776
- }
777
- /** Returns active fields sorted by Sequence for a given IntegrationObject. */
778
- getCachedFields(objectID) {
779
- return IntegrationEngineBase.Instance.GetIntegrationObjectFields(objectID)
780
- .filter(f => f.Status === 'Active')
781
- .sort((a, b) => a.Sequence - b.Sequence);
782
- }
783
- /** Builds the static ExternalFieldSchema list from cached metadata. */
784
- getStaticFieldSchemas(integrationID, objectName) {
785
- const obj = this.tryGetCachedObject(integrationID, objectName);
786
- if (!obj)
787
- return [];
788
- const fields = this.getCachedFields(obj.ID);
789
- return fields.map(f => ({
790
- Name: f.Name,
791
- Label: f.DisplayName ?? f.Name,
792
- Description: f.Description ?? undefined,
793
- DataType: f.Type,
794
- IsRequired: f.IsRequired,
795
- IsUniqueKey: f.IsUniqueKey || f.IsPrimaryKey,
796
- IsReadOnly: f.IsReadOnly,
797
- IsForeignKey: f.RelatedIntegrationObjectID != null,
798
- ForeignKeyTarget: f.RelatedIntegrationObject ?? null,
799
- }));
800
- }
801
- /** Returns the primary key field names sorted by Sequence; falls back to `['ID']`. */
802
- findPrimaryKeyFieldNames(fields) {
803
- const pk = fields.filter(f => f.IsPrimaryKey).sort((a, b) => a.Sequence - b.Sequence);
804
- return pk.length > 0 ? pk.map(f => f.Name) : ['ID'];
805
- }
806
- // ─── Record / watermark conversion ───────────────────────────────
807
- /** Converts a raw GraphQL row into the framework's ExternalRecord. */
808
- toExternalRecord(raw, objectType, pkFieldNames) {
809
- const externalID = pkFieldNames
810
- .map(name => raw[name] != null ? String(raw[name]) : '')
811
- .join('|');
812
- return { ExternalID: externalID, ObjectType: objectType, Fields: raw };
813
- }
814
- /**
815
- * Computes the watermark to advance to after this batch. Returns the
816
- * greatest watermark-field value among the fetched rows (lexicographic
817
- * comparison works for ISO-8601 timestamps), falling back to the
818
- * previous value so we never regress.
819
- */
820
- computeNewWatermark(rows, watermarkField, previous) {
821
- if (!watermarkField || rows.length === 0)
822
- return previous;
823
- let max = previous;
824
- for (const row of rows) {
825
- const raw = row[watermarkField];
826
- if (raw == null)
827
- continue;
828
- const asString = raw instanceof Date ? raw.toISOString() : String(raw);
829
- if (max == null || asString > max)
830
- max = asString;
831
- }
832
- return max;
833
- }
834
- // ─── String helpers ──────────────────────────────────────────────
835
- /** Parses JSON safely — returns null on malformed input. */
836
- safeParseJson(json) {
837
- if (!json)
838
- return null;
839
- try {
840
- const parsed = JSON.parse(json);
841
- return (typeof parsed === 'object' && parsed != null) ? parsed : null;
842
- }
843
- catch {
844
- return null;
845
- }
846
- }
847
- /** Returns the first defined non-empty string value among the given keys. */
848
- pickString(source, ...keys) {
849
- for (const key of keys) {
850
- const value = source[key];
851
- if (typeof value === 'string' && value.trim().length > 0)
852
- return value.trim();
853
- }
854
- return undefined;
855
- }
856
- /** Returns the first positive integer value found among the given keys. */
857
- pickPositiveInt(source, ...keys) {
858
- for (const key of keys) {
859
- const value = source[key];
860
- if (value == null)
861
- continue;
862
- const n = Number(value);
863
- if (!Number.isNaN(n) && Number.isFinite(n) && n > 0)
864
- return Math.floor(n);
865
- }
866
- return undefined;
867
- }
868
- /** Removes a trailing slash if present. */
869
- stripTrailingSlash(url) {
870
- return url.endsWith('/') ? url.slice(0, -1) : url;
871
- }
872
- /**
873
- * Strips an "MJ:" / "MJ_" prefix from a name so introspection sees the
874
- * underlying type name (e.g., "MJ_AIAgentRuns" -> "AIAgentRuns").
875
- */
876
- stripMJPrefix(name) {
877
- const trimmed = name.trim();
878
- if (/^MJ[_:]/.test(trimmed))
879
- return trimmed.replace(/^MJ[_:]/, '');
880
- return trimmed;
881
- }
882
- /** Builds a lowercased Set from a string[] for case-insensitive containment checks. */
883
- toLowerSet(values) {
884
- return new Set(values.map(v => v.toLowerCase()));
885
- }
886
- };
887
- MJToMJConnector = __decorate([
888
- RegisterClass(BaseIntegrationConnector, 'MJToMJConnector')
889
- ], MJToMJConnector);
890
- export { MJToMJConnector };
891
- /** Tree-shaking prevention function — import and call from module entry point. */
892
- export function LoadMJToMJConnector() { }
893
- //# sourceMappingURL=MJToMJConnector.js.map