@memberjunction/integration-connectors 5.51.0 → 6.1.0-edge.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +109 -277
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -38
- package/dist/index.js.map +1 -1
- package/package.json +14 -19
- package/dist/AptifyConnector.d.ts +0 -95
- package/dist/AptifyConnector.d.ts.map +0 -1
- package/dist/AptifyConnector.js +0 -760
- package/dist/AptifyConnector.js.map +0 -1
- package/dist/BlackbaudConnector.d.ts +0 -64
- package/dist/BlackbaudConnector.d.ts.map +0 -1
- package/dist/BlackbaudConnector.js +0 -916
- package/dist/BlackbaudConnector.js.map +0 -1
- package/dist/ConstantContactConnector.d.ts +0 -137
- package/dist/ConstantContactConnector.d.ts.map +0 -1
- package/dist/ConstantContactConnector.js +0 -663
- package/dist/ConstantContactConnector.js.map +0 -1
- package/dist/CventConnector.d.ts +0 -146
- package/dist/CventConnector.d.ts.map +0 -1
- package/dist/CventConnector.js +0 -540
- package/dist/CventConnector.js.map +0 -1
- package/dist/DynamicsDataverseConnector.d.ts +0 -296
- package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
- package/dist/DynamicsDataverseConnector.js +0 -790
- package/dist/DynamicsDataverseConnector.js.map +0 -1
- package/dist/FileFeedConnector.d.ts +0 -64
- package/dist/FileFeedConnector.d.ts.map +0 -1
- package/dist/FileFeedConnector.js +0 -190
- package/dist/FileFeedConnector.js.map +0 -1
- package/dist/FontevaConnector.d.ts +0 -153
- package/dist/FontevaConnector.d.ts.map +0 -1
- package/dist/FontevaConnector.js +0 -421
- package/dist/FontevaConnector.js.map +0 -1
- package/dist/GrowthZoneConnector.d.ts +0 -183
- package/dist/GrowthZoneConnector.d.ts.map +0 -1
- package/dist/GrowthZoneConnector.js +0 -702
- package/dist/GrowthZoneConnector.js.map +0 -1
- package/dist/HivebriteConnector.d.ts +0 -151
- package/dist/HivebriteConnector.d.ts.map +0 -1
- package/dist/HivebriteConnector.js +0 -533
- package/dist/HivebriteConnector.js.map +0 -1
- package/dist/HubSpotConnector.d.ts +0 -523
- package/dist/HubSpotConnector.d.ts.map +0 -1
- package/dist/HubSpotConnector.js +0 -3110
- package/dist/HubSpotConnector.js.map +0 -1
- package/dist/IMISConnector.d.ts +0 -171
- package/dist/IMISConnector.d.ts.map +0 -1
- package/dist/IMISConnector.js +0 -695
- package/dist/IMISConnector.js.map +0 -1
- package/dist/MJToMJConnector.d.ts +0 -279
- package/dist/MJToMJConnector.d.ts.map +0 -1
- package/dist/MJToMJConnector.js +0 -893
- package/dist/MJToMJConnector.js.map +0 -1
- package/dist/MagnetMailConnector.d.ts +0 -163
- package/dist/MagnetMailConnector.d.ts.map +0 -1
- package/dist/MagnetMailConnector.js +0 -823
- package/dist/MagnetMailConnector.js.map +0 -1
- package/dist/MailchimpConnector.d.ts +0 -197
- package/dist/MailchimpConnector.d.ts.map +0 -1
- package/dist/MailchimpConnector.js +0 -791
- package/dist/MailchimpConnector.js.map +0 -1
- package/dist/MemberSuiteConnector.d.ts +0 -185
- package/dist/MemberSuiteConnector.d.ts.map +0 -1
- package/dist/MemberSuiteConnector.js +0 -762
- package/dist/MemberSuiteConnector.js.map +0 -1
- package/dist/NeonCRMConnector.d.ts +0 -273
- package/dist/NeonCRMConnector.d.ts.map +0 -1
- package/dist/NeonCRMConnector.js +0 -1039
- package/dist/NeonCRMConnector.js.map +0 -1
- package/dist/NetForumConnector.d.ts +0 -170
- package/dist/NetForumConnector.d.ts.map +0 -1
- package/dist/NetForumConnector.js +0 -778
- package/dist/NetForumConnector.js.map +0 -1
- package/dist/NetSuiteConnector.d.ts +0 -136
- package/dist/NetSuiteConnector.d.ts.map +0 -1
- package/dist/NetSuiteConnector.js +0 -703
- package/dist/NetSuiteConnector.js.map +0 -1
- package/dist/NimbleAMSConnector.d.ts +0 -142
- package/dist/NimbleAMSConnector.d.ts.map +0 -1
- package/dist/NimbleAMSConnector.js +0 -703
- package/dist/NimbleAMSConnector.js.map +0 -1
- package/dist/NoviConnector.d.ts +0 -178
- package/dist/NoviConnector.d.ts.map +0 -1
- package/dist/NoviConnector.js +0 -566
- package/dist/NoviConnector.js.map +0 -1
- package/dist/ORCIDConnector.d.ts +0 -207
- package/dist/ORCIDConnector.d.ts.map +0 -1
- package/dist/ORCIDConnector.js +0 -708
- package/dist/ORCIDConnector.js.map +0 -1
- package/dist/OpenWaterConnector.d.ts +0 -173
- package/dist/OpenWaterConnector.d.ts.map +0 -1
- package/dist/OpenWaterConnector.js +0 -893
- package/dist/OpenWaterConnector.js.map +0 -1
- package/dist/PathLMSConnector.d.ts +0 -395
- package/dist/PathLMSConnector.d.ts.map +0 -1
- package/dist/PathLMSConnector.js +0 -1281
- package/dist/PathLMSConnector.js.map +0 -1
- package/dist/PheedLoopConnector.d.ts +0 -119
- package/dist/PheedLoopConnector.d.ts.map +0 -1
- package/dist/PheedLoopConnector.js +0 -429
- package/dist/PheedLoopConnector.js.map +0 -1
- package/dist/PropFuelConnector.d.ts +0 -153
- package/dist/PropFuelConnector.d.ts.map +0 -1
- package/dist/PropFuelConnector.js +0 -450
- package/dist/PropFuelConnector.js.map +0 -1
- package/dist/QuickBooksConnector.d.ts +0 -92
- package/dist/QuickBooksConnector.d.ts.map +0 -1
- package/dist/QuickBooksConnector.js +0 -713
- package/dist/QuickBooksConnector.js.map +0 -1
- package/dist/RasaConnector.d.ts +0 -152
- package/dist/RasaConnector.d.ts.map +0 -1
- package/dist/RasaConnector.js +0 -929
- package/dist/RasaConnector.js.map +0 -1
- package/dist/Reach360Connector.d.ts +0 -175
- package/dist/Reach360Connector.d.ts.map +0 -1
- package/dist/Reach360Connector.js +0 -796
- package/dist/Reach360Connector.js.map +0 -1
- package/dist/RelationalDBConnector.d.ts +0 -108
- package/dist/RelationalDBConnector.d.ts.map +0 -1
- package/dist/RelationalDBConnector.js +0 -228
- package/dist/RelationalDBConnector.js.map +0 -1
- package/dist/RhythmConnector.d.ts +0 -188
- package/dist/RhythmConnector.d.ts.map +0 -1
- package/dist/RhythmConnector.js +0 -784
- package/dist/RhythmConnector.js.map +0 -1
- package/dist/SageIntacctConnector.d.ts +0 -337
- package/dist/SageIntacctConnector.d.ts.map +0 -1
- package/dist/SageIntacctConnector.js +0 -2081
- package/dist/SageIntacctConnector.js.map +0 -1
- package/dist/SalesforceConnector.d.ts +0 -461
- package/dist/SalesforceConnector.d.ts.map +0 -1
- package/dist/SalesforceConnector.js +0 -1862
- package/dist/SalesforceConnector.js.map +0 -1
- package/dist/SharePointConnector.d.ts +0 -188
- package/dist/SharePointConnector.d.ts.map +0 -1
- package/dist/SharePointConnector.js +0 -716
- package/dist/SharePointConnector.js.map +0 -1
- package/dist/WicketConnector.d.ts +0 -252
- package/dist/WicketConnector.d.ts.map +0 -1
- package/dist/WicketConnector.js +0 -938
- package/dist/WicketConnector.js.map +0 -1
- package/dist/WildApricotConnector.d.ts +0 -201
- package/dist/WildApricotConnector.d.ts.map +0 -1
- package/dist/WildApricotConnector.js +0 -1129
- package/dist/WildApricotConnector.js.map +0 -1
- package/dist/YourMembershipConnector.d.ts +0 -349
- package/dist/YourMembershipConnector.d.ts.map +0 -1
- package/dist/YourMembershipConnector.js +0 -4062
- package/dist/YourMembershipConnector.js.map +0 -1
- package/dist/generate-integration-actions.d.ts +0 -23
- package/dist/generate-integration-actions.d.ts.map +0 -1
- package/dist/generate-integration-actions.js +0 -201
- package/dist/generate-integration-actions.js.map +0 -1
package/dist/MJToMJConnector.js
DELETED
|
@@ -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
|