@memberjunction/integration-connectors 5.35.0 → 5.37.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/dist/AptifyConnector.d.ts +88 -0
- package/dist/AptifyConnector.d.ts.map +1 -0
- package/dist/AptifyConnector.js +737 -0
- package/dist/AptifyConnector.js.map +1 -0
- package/dist/BettyConnector.d.ts +288 -0
- package/dist/BettyConnector.d.ts.map +1 -0
- package/dist/BettyConnector.js +909 -0
- package/dist/BettyConnector.js.map +1 -0
- package/dist/BlackbaudConnector.d.ts +64 -0
- package/dist/BlackbaudConnector.d.ts.map +1 -0
- package/dist/BlackbaudConnector.js +916 -0
- package/dist/BlackbaudConnector.js.map +1 -0
- package/dist/ConstantContactConnector.d.ts +126 -0
- package/dist/ConstantContactConnector.d.ts.map +1 -0
- package/dist/ConstantContactConnector.js +636 -0
- package/dist/ConstantContactConnector.js.map +1 -0
- package/dist/GrowthZoneConnector.d.ts +185 -0
- package/dist/GrowthZoneConnector.d.ts.map +1 -0
- package/dist/GrowthZoneConnector.js +745 -0
- package/dist/GrowthZoneConnector.js.map +1 -0
- package/dist/HubSpotConnector.d.ts +2 -2
- package/dist/HubSpotConnector.d.ts.map +1 -1
- package/dist/HubSpotConnector.js +3 -3
- package/dist/HubSpotConnector.js.map +1 -1
- package/dist/IMISConnector.d.ts +155 -0
- package/dist/IMISConnector.d.ts.map +1 -0
- package/dist/IMISConnector.js +686 -0
- package/dist/IMISConnector.js.map +1 -0
- package/dist/MJToMJConnector.d.ts +279 -0
- package/dist/MJToMJConnector.d.ts.map +1 -0
- package/dist/MJToMJConnector.js +893 -0
- package/dist/MJToMJConnector.js.map +1 -0
- package/dist/MagnetMailConnector.d.ts +163 -0
- package/dist/MagnetMailConnector.d.ts.map +1 -0
- package/dist/MagnetMailConnector.js +816 -0
- package/dist/MagnetMailConnector.js.map +1 -0
- package/dist/MailchimpConnector.d.ts +188 -0
- package/dist/MailchimpConnector.d.ts.map +1 -0
- package/dist/MailchimpConnector.js +753 -0
- package/dist/MailchimpConnector.js.map +1 -0
- package/dist/NetForumConnector.d.ts +51 -0
- package/dist/NetForumConnector.d.ts.map +1 -0
- package/dist/NetForumConnector.js +419 -0
- package/dist/NetForumConnector.js.map +1 -0
- package/dist/NetSuiteConnector.d.ts +70 -0
- package/dist/NetSuiteConnector.d.ts.map +1 -0
- package/dist/NetSuiteConnector.js +605 -0
- package/dist/NetSuiteConnector.js.map +1 -0
- package/dist/NimbleAMSConnector.d.ts +49 -0
- package/dist/NimbleAMSConnector.d.ts.map +1 -0
- package/dist/NimbleAMSConnector.js +408 -0
- package/dist/NimbleAMSConnector.js.map +1 -0
- package/dist/PropFuelConnector.d.ts +68 -0
- package/dist/PropFuelConnector.d.ts.map +1 -0
- package/dist/PropFuelConnector.js +514 -0
- package/dist/PropFuelConnector.js.map +1 -0
- package/dist/RasaConnector.d.ts +26 -0
- package/dist/RasaConnector.d.ts.map +1 -1
- package/dist/RasaConnector.js +233 -1
- package/dist/RasaConnector.js.map +1 -1
- package/dist/Reach360Connector.d.ts +165 -0
- package/dist/Reach360Connector.d.ts.map +1 -0
- package/dist/Reach360Connector.js +710 -0
- package/dist/Reach360Connector.js.map +1 -0
- package/dist/SharePointConnector.d.ts +181 -0
- package/dist/SharePointConnector.d.ts.map +1 -0
- package/dist/SharePointConnector.js +796 -0
- package/dist/SharePointConnector.js.map +1 -0
- package/dist/WicketConnector.d.ts +1 -0
- package/dist/WicketConnector.d.ts.map +1 -1
- package/dist/WicketConnector.js +6 -0
- package/dist/WicketConnector.js.map +1 -1
- package/dist/WildApricotConnector.d.ts +188 -0
- package/dist/WildApricotConnector.d.ts.map +1 -0
- package/dist/WildApricotConnector.js +1092 -0
- package/dist/WildApricotConnector.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
8
|
+
import { Metadata } from '@memberjunction/core';
|
|
9
|
+
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
10
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
11
|
+
/** Default Graph base URL (production tenant, v1.0). */
|
|
12
|
+
const GRAPH_V1_BASE_URL = 'https://graph.microsoft.com/v1.0';
|
|
13
|
+
/** Default OAuth scope for app-only Graph access. */
|
|
14
|
+
const DEFAULT_SCOPE = 'https://graph.microsoft.com/.default';
|
|
15
|
+
/** Azure AD token endpoint template. */
|
|
16
|
+
const AAD_TOKEN_ENDPOINT = (tenantId) => `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0/token`;
|
|
17
|
+
/** Default minimum interval between API calls (Graph baseline is ~4 req/s). */
|
|
18
|
+
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 250;
|
|
19
|
+
/** Default HTTP request timeout. */
|
|
20
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
21
|
+
/** Default retry count for retryable errors. */
|
|
22
|
+
const DEFAULT_MAX_RETRIES = 5;
|
|
23
|
+
/** Buffer before token expiry at which to refresh (60s). */
|
|
24
|
+
const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;
|
|
25
|
+
/** Object name used to match the SiteListColumns IntegrationObject during runtime field discovery. */
|
|
26
|
+
const LIST_ITEM_OBJECT = 'SiteListItems';
|
|
27
|
+
// ─── SharePoint Object Metadata for Action Generation ─────────────────
|
|
28
|
+
const SHAREPOINT_OBJECTS = [
|
|
29
|
+
{
|
|
30
|
+
Name: 'Sites',
|
|
31
|
+
DisplayName: 'Site',
|
|
32
|
+
Description: 'A SharePoint site (team, communication, hub, or personal OneDrive site).',
|
|
33
|
+
SupportsWrite: false,
|
|
34
|
+
Fields: [
|
|
35
|
+
{ Name: 'id', DisplayName: 'Site ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Composite site identifier (hostname,site-collection-guid,web-guid).' },
|
|
36
|
+
{ Name: 'displayName', DisplayName: 'Display Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Human-readable site name.' },
|
|
37
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Internal site name.' },
|
|
38
|
+
{ Name: 'webUrl', DisplayName: 'Web URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Full URL to the site.' },
|
|
39
|
+
{ Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Site description.' },
|
|
40
|
+
{ Name: 'createdDateTime', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Site creation timestamp.' },
|
|
41
|
+
{ Name: 'lastModifiedDateTime', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modification timestamp.' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
Name: 'SiteLists',
|
|
46
|
+
DisplayName: 'List',
|
|
47
|
+
Description: 'A list within a SharePoint site (custom list, document library, or calendar).',
|
|
48
|
+
SupportsWrite: true,
|
|
49
|
+
Fields: [
|
|
50
|
+
{ Name: 'id', DisplayName: 'List ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'List GUID.' },
|
|
51
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Internal list name.' },
|
|
52
|
+
{ Name: 'displayName', DisplayName: 'Display Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'List title shown in the UI.' },
|
|
53
|
+
{ Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'List description.' },
|
|
54
|
+
{ Name: 'webUrl', DisplayName: 'Web URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'URL to the list in SharePoint.' },
|
|
55
|
+
{ Name: 'lastModifiedDateTime', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modification timestamp.' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
Name: 'SiteListItems',
|
|
60
|
+
DisplayName: 'List Item',
|
|
61
|
+
Description: 'A row within a SharePoint list. Dynamic custom columns live in the fields dictionary.',
|
|
62
|
+
SupportsWrite: true,
|
|
63
|
+
Fields: [
|
|
64
|
+
{ Name: 'id', DisplayName: 'Item ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Per-list auto-increment integer.' },
|
|
65
|
+
{ Name: 'createdDateTime', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Item creation timestamp.' },
|
|
66
|
+
{ Name: 'lastModifiedDateTime', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Item modification timestamp.' },
|
|
67
|
+
{ Name: 'webUrl', DisplayName: 'Web URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Full URL to the item.' },
|
|
68
|
+
{ Name: 'fields', DisplayName: 'Fields', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Dynamic dictionary keyed by list column names.' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
Name: 'Drives',
|
|
73
|
+
DisplayName: 'Drive',
|
|
74
|
+
Description: 'A SharePoint document library or OneDrive personal drive.',
|
|
75
|
+
SupportsWrite: false,
|
|
76
|
+
Fields: [
|
|
77
|
+
{ Name: 'id', DisplayName: 'Drive ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Drive GUID.' },
|
|
78
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Drive name.' },
|
|
79
|
+
{ Name: 'driveType', DisplayName: 'Drive Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'personal, business, or documentLibrary.' },
|
|
80
|
+
{ Name: 'webUrl', DisplayName: 'Web URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'URL to the drive.' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
Name: 'DriveItems',
|
|
85
|
+
DisplayName: 'Drive Item',
|
|
86
|
+
Description: 'A file or folder in a SharePoint drive.',
|
|
87
|
+
SupportsWrite: true,
|
|
88
|
+
Fields: [
|
|
89
|
+
{ Name: 'id', DisplayName: 'Item ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Drive item GUID.' },
|
|
90
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'File or folder name.' },
|
|
91
|
+
{ Name: 'size', DisplayName: 'Size', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'File size in bytes.' },
|
|
92
|
+
{ Name: 'webUrl', DisplayName: 'Web URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Full URL to the item.' },
|
|
93
|
+
{ Name: 'lastModifiedDateTime', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Item modification timestamp.' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
// ─── Connector Implementation ────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Connector for Microsoft SharePoint Online via Microsoft Graph v1.0.
|
|
100
|
+
*
|
|
101
|
+
* Extends BaseRESTIntegrationConnector — inherits pagination + template variable
|
|
102
|
+
* substitution from metadata. Template vars like `{id}` / `{siteId}` are resolved
|
|
103
|
+
* per-parent using IntegrationObject FK / PK metadata, so object hierarchies
|
|
104
|
+
* (sites -> lists -> list items) traverse automatically.
|
|
105
|
+
*
|
|
106
|
+
* Auth flow:
|
|
107
|
+
* 1. Client-credentials OAuth 2.0 against Azure AD
|
|
108
|
+
* 2. Bearer token attached to every Graph request
|
|
109
|
+
* 3. Token is refreshed proactively before expiry
|
|
110
|
+
*
|
|
111
|
+
* Pagination: `@odata.nextLink` cursor (per OData convention).
|
|
112
|
+
*
|
|
113
|
+
* Incremental sync: `/delta?token=...` endpoints for drives and list items.
|
|
114
|
+
*/
|
|
115
|
+
let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationConnector {
|
|
116
|
+
constructor() {
|
|
117
|
+
// ── State ────────────────────────────────────────────────────────
|
|
118
|
+
super(...arguments);
|
|
119
|
+
this.cachedAuth = null;
|
|
120
|
+
this.tokenExpiresAt = 0;
|
|
121
|
+
this.lastRequestTime = 0;
|
|
122
|
+
}
|
|
123
|
+
// ── Capability Getters ───────────────────────────────────────────
|
|
124
|
+
get SupportsCreate() { return true; }
|
|
125
|
+
get SupportsUpdate() { return true; }
|
|
126
|
+
get SupportsDelete() { return true; }
|
|
127
|
+
get IntegrationName() { return 'SharePoint'; }
|
|
128
|
+
// ── Action Generation ────────────────────────────────────────────
|
|
129
|
+
GetIntegrationObjects() {
|
|
130
|
+
return SHAREPOINT_OBJECTS;
|
|
131
|
+
}
|
|
132
|
+
GetActionGeneratorConfig() {
|
|
133
|
+
const objects = this.GetIntegrationObjects();
|
|
134
|
+
if (objects.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return {
|
|
137
|
+
IntegrationName: 'SharePoint',
|
|
138
|
+
CategoryName: 'SharePoint',
|
|
139
|
+
IconClass: 'fa-brands fa-microsoft',
|
|
140
|
+
Objects: objects,
|
|
141
|
+
IncludeSearch: false,
|
|
142
|
+
IncludeList: false,
|
|
143
|
+
CategoryDescription: 'Microsoft SharePoint integration actions (via Graph)',
|
|
144
|
+
ParentCategoryName: 'Business Apps',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ── Default Configuration ────────────────────────────────────────
|
|
148
|
+
GetDefaultConfiguration() {
|
|
149
|
+
return {
|
|
150
|
+
DefaultSchemaName: 'SharePoint',
|
|
151
|
+
DefaultObjects: [
|
|
152
|
+
{
|
|
153
|
+
SourceObjectName: 'Sites',
|
|
154
|
+
TargetTableName: 'SharePoint_Site',
|
|
155
|
+
TargetEntityName: 'SharePoint Sites',
|
|
156
|
+
SyncEnabled: true,
|
|
157
|
+
FieldMappings: this.GetDefaultFieldMappings('Sites', 'Contacts'),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
SourceObjectName: 'SiteLists',
|
|
161
|
+
TargetTableName: 'SharePoint_List',
|
|
162
|
+
TargetEntityName: 'SharePoint Lists',
|
|
163
|
+
SyncEnabled: true,
|
|
164
|
+
FieldMappings: this.GetDefaultFieldMappings('SiteLists', 'Lists'),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
SourceObjectName: 'SiteListItems',
|
|
168
|
+
TargetTableName: 'SharePoint_ListItem',
|
|
169
|
+
TargetEntityName: 'SharePoint List Items',
|
|
170
|
+
SyncEnabled: true,
|
|
171
|
+
FieldMappings: this.GetDefaultFieldMappings('SiteListItems', 'ListItems'),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
GetDefaultFieldMappings(objectName, _entityName) {
|
|
177
|
+
switch (objectName) {
|
|
178
|
+
case 'Sites':
|
|
179
|
+
return [
|
|
180
|
+
{ SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
181
|
+
{ SourceFieldName: 'displayName', DestinationFieldName: 'Name' },
|
|
182
|
+
{ SourceFieldName: 'webUrl', DestinationFieldName: 'URL' },
|
|
183
|
+
{ SourceFieldName: 'description', DestinationFieldName: 'Description' },
|
|
184
|
+
];
|
|
185
|
+
case 'SiteLists':
|
|
186
|
+
return [
|
|
187
|
+
{ SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
188
|
+
{ SourceFieldName: 'displayName', DestinationFieldName: 'Name' },
|
|
189
|
+
{ SourceFieldName: 'description', DestinationFieldName: 'Description' },
|
|
190
|
+
];
|
|
191
|
+
case 'SiteListItems':
|
|
192
|
+
return [
|
|
193
|
+
{ SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
194
|
+
];
|
|
195
|
+
default:
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ── TestConnection ───────────────────────────────────────────────
|
|
200
|
+
/**
|
|
201
|
+
* Verifies the service principal can reach Graph by fetching the tenant's
|
|
202
|
+
* organization record. Forces a token refresh so credentials are validated.
|
|
203
|
+
*/
|
|
204
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
205
|
+
try {
|
|
206
|
+
const auth = await this.Authenticate(companyIntegration, contextUser, true);
|
|
207
|
+
const url = `${auth.BaseUrl}/organization?$select=id,displayName`;
|
|
208
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
209
|
+
this.ValidateResponse(response, url);
|
|
210
|
+
const body = response.Body;
|
|
211
|
+
const orgName = body.value?.[0]?.displayName ?? 'unknown';
|
|
212
|
+
return {
|
|
213
|
+
Success: true,
|
|
214
|
+
Message: `Successfully connected to Microsoft Graph (tenant: ${orgName})`,
|
|
215
|
+
ServerVersion: 'Microsoft Graph v1.0',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
220
|
+
return { Success: false, Message: `Connection failed: ${message}` };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ── DiscoverObjects / DiscoverFields ─────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Returns the static IntegrationObject list. Discovery of per-site custom
|
|
226
|
+
* lists happens via the SiteLists IntegrationObject's FetchChanges loop, not
|
|
227
|
+
* here — this is a capability catalog, not a live enumeration.
|
|
228
|
+
*/
|
|
229
|
+
async DiscoverObjects(companyIntegration, contextUser) {
|
|
230
|
+
return super.DiscoverObjects(companyIntegration, contextUser);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Returns static field metadata from IntegrationEngineBase, plus — for
|
|
234
|
+
* SiteListItems — augments with live list-column discovery per list.
|
|
235
|
+
* Custom columns are marked via the IsReadOnly=false / isSealed=false
|
|
236
|
+
* Graph-reported flags; callers downstream can set IsCustom accordingly.
|
|
237
|
+
*/
|
|
238
|
+
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
239
|
+
const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
|
|
240
|
+
if (objectName !== LIST_ITEM_OBJECT)
|
|
241
|
+
return staticFields;
|
|
242
|
+
const custom = await this.DiscoverListColumnsForAllLists(companyIntegration, contextUser, staticFields);
|
|
243
|
+
return [...staticFields, ...custom];
|
|
244
|
+
}
|
|
245
|
+
// ── FetchChanges (Template Variable / Delta hooks) ───────────────
|
|
246
|
+
/**
|
|
247
|
+
* Top-level FetchChanges delegates to BaseRESTIntegrationConnector for
|
|
248
|
+
* template-variable resolution and pagination. We intercept the top-level
|
|
249
|
+
* Sites endpoint because the /sites?search=* syntax is not a standard GET
|
|
250
|
+
* collection — we normalise its response to the value array ourselves.
|
|
251
|
+
*/
|
|
252
|
+
async FetchChanges(ctx) {
|
|
253
|
+
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
254
|
+
// Delta sync path — when watermark is present and object supports it,
|
|
255
|
+
// use /delta endpoints rather than full re-enumeration.
|
|
256
|
+
if (ctx.WatermarkValue && obj.SupportsIncrementalSync && this.SupportsDeltaForObject(obj)) {
|
|
257
|
+
return this.FetchChangesViaDelta(ctx, obj);
|
|
258
|
+
}
|
|
259
|
+
return super.FetchChanges(ctx);
|
|
260
|
+
}
|
|
261
|
+
// ── CRUD operations ──────────────────────────────────────────────
|
|
262
|
+
async CreateRecord(ctx) {
|
|
263
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
264
|
+
const contextUser = ctx.ContextUser;
|
|
265
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
266
|
+
try {
|
|
267
|
+
const url = this.BuildCRUDUrl(auth, companyIntegration.IntegrationID, ctx.ObjectName, null);
|
|
268
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', this.BuildHeaders(auth), ctx.Attributes);
|
|
269
|
+
this.ValidateResponse(response, url);
|
|
270
|
+
const body = response.Body;
|
|
271
|
+
return { Success: true, ExternalID: String(body.id ?? ''), StatusCode: response.Status };
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
return this.BuildCRUDError(err, 'CreateRecord', ctx.ObjectName);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async UpdateRecord(ctx) {
|
|
278
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
279
|
+
const contextUser = ctx.ContextUser;
|
|
280
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
281
|
+
try {
|
|
282
|
+
const url = this.BuildCRUDUrl(auth, companyIntegration.IntegrationID, ctx.ObjectName, ctx.ExternalID);
|
|
283
|
+
const response = await this.MakeHTTPRequest(auth, url, 'PATCH', this.BuildHeaders(auth), ctx.Attributes);
|
|
284
|
+
this.ValidateResponse(response, url);
|
|
285
|
+
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
return this.BuildCRUDError(err, 'UpdateRecord', ctx.ObjectName);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async DeleteRecord(ctx) {
|
|
292
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
293
|
+
const contextUser = ctx.ContextUser;
|
|
294
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
295
|
+
try {
|
|
296
|
+
const url = this.BuildCRUDUrl(auth, companyIntegration.IntegrationID, ctx.ObjectName, ctx.ExternalID);
|
|
297
|
+
const response = await this.MakeHTTPRequest(auth, url, 'DELETE', this.BuildHeaders(auth));
|
|
298
|
+
if (response.Status === 404) {
|
|
299
|
+
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: 404 };
|
|
300
|
+
}
|
|
301
|
+
this.ValidateResponse(response, url);
|
|
302
|
+
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
return this.BuildCRUDError(err, 'DeleteRecord', ctx.ObjectName);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async GetRecord(ctx) {
|
|
309
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
310
|
+
const contextUser = ctx.ContextUser;
|
|
311
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
312
|
+
try {
|
|
313
|
+
const url = this.BuildCRUDUrl(auth, companyIntegration.IntegrationID, ctx.ObjectName, ctx.ExternalID);
|
|
314
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
315
|
+
if (response.Status === 404)
|
|
316
|
+
return null;
|
|
317
|
+
this.ValidateResponse(response, url);
|
|
318
|
+
const body = response.Body;
|
|
319
|
+
return {
|
|
320
|
+
ExternalID: String(body['id'] ?? ctx.ExternalID),
|
|
321
|
+
ObjectType: ctx.ObjectName,
|
|
322
|
+
Fields: body,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ─── Abstract BaseRESTIntegrationConnector hooks ────────────────
|
|
330
|
+
/**
|
|
331
|
+
* Authenticates with Azure AD via client-credentials flow.
|
|
332
|
+
* Caches the bearer token until within TOKEN_REFRESH_BUFFER_MS of expiry.
|
|
333
|
+
*/
|
|
334
|
+
async Authenticate(companyIntegration, contextUser, forceRefresh = false) {
|
|
335
|
+
if (!forceRefresh && this.cachedAuth && this.IsTokenValid()) {
|
|
336
|
+
return this.cachedAuth;
|
|
337
|
+
}
|
|
338
|
+
const config = await this.ParseConfig(companyIntegration, contextUser);
|
|
339
|
+
const token = await this.RequestGraphToken(config);
|
|
340
|
+
const baseUrl = config.GraphBaseUrl ?? GRAPH_V1_BASE_URL;
|
|
341
|
+
const auth = {
|
|
342
|
+
Token: token.access_token,
|
|
343
|
+
ExpiresAt: new Date(Date.now() + token.expires_in * 1000),
|
|
344
|
+
Config: config,
|
|
345
|
+
BaseUrl: baseUrl,
|
|
346
|
+
};
|
|
347
|
+
this.cachedAuth = auth;
|
|
348
|
+
this.tokenExpiresAt = Date.now() + token.expires_in * 1000;
|
|
349
|
+
return auth;
|
|
350
|
+
}
|
|
351
|
+
BuildHeaders(auth) {
|
|
352
|
+
const token = auth.Token ?? '';
|
|
353
|
+
return {
|
|
354
|
+
'Authorization': `Bearer ${token}`,
|
|
355
|
+
'Accept': 'application/json',
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* HTTP transport with rate-limit throttling, Retry-After honoring, and
|
|
361
|
+
* exponential backoff on 429/503.
|
|
362
|
+
*/
|
|
363
|
+
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
364
|
+
const spAuth = auth;
|
|
365
|
+
const config = spAuth.Config;
|
|
366
|
+
const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
367
|
+
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
368
|
+
const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
369
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
370
|
+
await this.Throttle(minInterval);
|
|
371
|
+
const response = await this.ExecuteOneRequest(url, method, headers, body, timeoutMs);
|
|
372
|
+
if (this.IsRetryable(response) && attempt < maxRetries) {
|
|
373
|
+
const delay = this.ComputeBackoffDelay(attempt, response.Headers['retry-after']);
|
|
374
|
+
await this.Sleep(delay);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
return response;
|
|
378
|
+
}
|
|
379
|
+
throw new Error(`SharePointConnector: exhausted ${maxRetries + 1} attempts for ${method} ${url}`);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Extracts the `value` array from a Graph response envelope. The base class
|
|
383
|
+
* already passes `ResponseDataKey` from metadata — we default to the `value`
|
|
384
|
+
* convention when responseDataKey is null.
|
|
385
|
+
*/
|
|
386
|
+
NormalizeResponse(rawBody, responseDataKey) {
|
|
387
|
+
const body = rawBody;
|
|
388
|
+
const key = responseDataKey ?? 'value';
|
|
389
|
+
const data = body[key];
|
|
390
|
+
if (Array.isArray(data))
|
|
391
|
+
return data;
|
|
392
|
+
// Single-record response fallback
|
|
393
|
+
if (body && typeof body === 'object' && 'id' in body)
|
|
394
|
+
return [body];
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Extracts pagination state from the Graph `@odata.nextLink` annotation.
|
|
399
|
+
* Graph uses opaque URL cursors — we store the full nextLink URL in the
|
|
400
|
+
* cursor so the next iteration issues the exact request Graph expects.
|
|
401
|
+
*/
|
|
402
|
+
ExtractPaginationInfo(rawBody, _paginationType, _currentPage, _currentOffset, _pageSize) {
|
|
403
|
+
const body = rawBody;
|
|
404
|
+
const nextLink = body?.['@odata.nextLink'];
|
|
405
|
+
return {
|
|
406
|
+
HasMore: typeof nextLink === 'string' && nextLink.length > 0,
|
|
407
|
+
NextCursor: nextLink,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
GetBaseURL(_companyIntegration, auth) {
|
|
411
|
+
const spAuth = auth;
|
|
412
|
+
return spAuth.BaseUrl;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Overrides the base pagination URL builder. Graph's nextLink is an absolute
|
|
416
|
+
* URL that encodes the full continuation request. When we have a cursor we
|
|
417
|
+
* use it directly; otherwise we rely on the base path (already wired with
|
|
418
|
+
* $top / $select via DefaultQueryParams).
|
|
419
|
+
*/
|
|
420
|
+
BuildPaginatedURL(basePath, obj, _page, _offset, cursor, effectivePageSize) {
|
|
421
|
+
if (cursor && cursor.length > 0)
|
|
422
|
+
return cursor;
|
|
423
|
+
const pageSize = effectivePageSize ?? obj.DefaultPageSize ?? 200;
|
|
424
|
+
const separator = basePath.includes('?') ? '&' : '?';
|
|
425
|
+
return `${basePath}${separator}$top=${pageSize}`;
|
|
426
|
+
}
|
|
427
|
+
// ─── Token Management ────────────────────────────────────────────
|
|
428
|
+
IsTokenValid() {
|
|
429
|
+
if (!this.cachedAuth || !this.cachedAuth.Token)
|
|
430
|
+
return false;
|
|
431
|
+
return Date.now() < this.tokenExpiresAt - TOKEN_REFRESH_BUFFER_MS;
|
|
432
|
+
}
|
|
433
|
+
async RequestGraphToken(config) {
|
|
434
|
+
const url = AAD_TOKEN_ENDPOINT(config.TenantId);
|
|
435
|
+
const scope = config.Scope ?? DEFAULT_SCOPE;
|
|
436
|
+
const form = new URLSearchParams({
|
|
437
|
+
client_id: config.ClientId,
|
|
438
|
+
scope,
|
|
439
|
+
client_secret: config.ClientSecret,
|
|
440
|
+
grant_type: 'client_credentials',
|
|
441
|
+
});
|
|
442
|
+
const response = await fetch(url, {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: {
|
|
445
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
446
|
+
'Accept': 'application/json',
|
|
447
|
+
},
|
|
448
|
+
body: form.toString(),
|
|
449
|
+
});
|
|
450
|
+
const text = await response.text();
|
|
451
|
+
if (!response.ok) {
|
|
452
|
+
throw new Error(`SharePointConnector: Azure AD token request failed (${response.status}): ${text.slice(0, 500)}`);
|
|
453
|
+
}
|
|
454
|
+
return JSON.parse(text);
|
|
455
|
+
}
|
|
456
|
+
// ─── Configuration Parsing ───────────────────────────────────────
|
|
457
|
+
async ParseConfig(companyIntegration, contextUser) {
|
|
458
|
+
const credentialID = companyIntegration.CredentialID;
|
|
459
|
+
if (credentialID) {
|
|
460
|
+
const config = await this.LoadFromCredentialEntity(credentialID, contextUser);
|
|
461
|
+
if (config)
|
|
462
|
+
return config;
|
|
463
|
+
}
|
|
464
|
+
const configJson = companyIntegration.Configuration;
|
|
465
|
+
if (configJson) {
|
|
466
|
+
const parsed = JSON.parse(configJson);
|
|
467
|
+
return this.ValidateConfig(parsed);
|
|
468
|
+
}
|
|
469
|
+
throw new Error('SharePointConnector: No credentials or configuration found on CompanyIntegration');
|
|
470
|
+
}
|
|
471
|
+
async LoadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
472
|
+
const md = provider ?? new Metadata();
|
|
473
|
+
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
474
|
+
const loaded = await credential.Load(credentialID);
|
|
475
|
+
if (!loaded || !credential.Values)
|
|
476
|
+
return null;
|
|
477
|
+
try {
|
|
478
|
+
const raw = JSON.parse(credential.Values);
|
|
479
|
+
const parsed = {
|
|
480
|
+
TenantId: raw.tenantId ?? raw.TenantId,
|
|
481
|
+
ClientId: raw.clientId ?? raw.ClientId,
|
|
482
|
+
ClientSecret: raw.clientSecret ?? raw.ClientSecret,
|
|
483
|
+
GraphBaseUrl: raw.graphBaseUrl ?? raw.GraphBaseUrl,
|
|
484
|
+
Scope: raw.scope ?? raw.Scope,
|
|
485
|
+
};
|
|
486
|
+
return this.ValidateConfig(parsed);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
ValidateConfig(raw) {
|
|
493
|
+
if (!raw.TenantId)
|
|
494
|
+
throw new Error('SharePointConnector: TenantId is required');
|
|
495
|
+
if (!raw.ClientId)
|
|
496
|
+
throw new Error('SharePointConnector: ClientId is required');
|
|
497
|
+
if (!raw.ClientSecret)
|
|
498
|
+
throw new Error('SharePointConnector: ClientSecret is required');
|
|
499
|
+
return {
|
|
500
|
+
TenantId: raw.TenantId,
|
|
501
|
+
ClientId: raw.ClientId,
|
|
502
|
+
ClientSecret: raw.ClientSecret,
|
|
503
|
+
GraphBaseUrl: raw.GraphBaseUrl,
|
|
504
|
+
Scope: raw.Scope,
|
|
505
|
+
MaxRetries: raw.MaxRetries ?? DEFAULT_MAX_RETRIES,
|
|
506
|
+
RequestTimeoutMs: raw.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
507
|
+
MinRequestIntervalMs: raw.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// ─── HTTP Helpers ────────────────────────────────────────────────
|
|
511
|
+
async ExecuteOneRequest(url, method, headers, body, timeoutMs) {
|
|
512
|
+
const controller = new AbortController();
|
|
513
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
514
|
+
try {
|
|
515
|
+
const response = await fetch(url, {
|
|
516
|
+
method,
|
|
517
|
+
headers,
|
|
518
|
+
body: body !== undefined && method !== 'GET' && method !== 'DELETE'
|
|
519
|
+
? JSON.stringify(body)
|
|
520
|
+
: undefined,
|
|
521
|
+
signal: controller.signal,
|
|
522
|
+
});
|
|
523
|
+
this.lastRequestTime = Date.now();
|
|
524
|
+
const responseHeaders = this.ExtractHeaders(response.headers);
|
|
525
|
+
const parsedBody = await this.ParseResponseBody(response);
|
|
526
|
+
return { Status: response.status, Body: parsedBody, Headers: responseHeaders };
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
clearTimeout(timeoutHandle);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async ParseResponseBody(response) {
|
|
533
|
+
const text = await response.text();
|
|
534
|
+
if (!text)
|
|
535
|
+
return null;
|
|
536
|
+
try {
|
|
537
|
+
return JSON.parse(text);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return text;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
ExtractHeaders(headers) {
|
|
544
|
+
const map = {};
|
|
545
|
+
headers.forEach((value, key) => {
|
|
546
|
+
map[key.toLowerCase()] = value;
|
|
547
|
+
});
|
|
548
|
+
return map;
|
|
549
|
+
}
|
|
550
|
+
IsRetryable(response) {
|
|
551
|
+
return response.Status === 429 || response.Status === 503 || response.Status === 504;
|
|
552
|
+
}
|
|
553
|
+
ComputeBackoffDelay(attempt, retryAfterHeader) {
|
|
554
|
+
if (retryAfterHeader) {
|
|
555
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
556
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
557
|
+
return Math.min(parsed * 1000, 60000);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return Math.min(Math.pow(2, attempt) * 1000, 30000);
|
|
561
|
+
}
|
|
562
|
+
async Throttle(minIntervalMs) {
|
|
563
|
+
const elapsed = Date.now() - this.lastRequestTime;
|
|
564
|
+
if (elapsed < minIntervalMs) {
|
|
565
|
+
await this.Sleep(minIntervalMs - elapsed);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
Sleep(ms) {
|
|
569
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
570
|
+
}
|
|
571
|
+
ValidateResponse(response, url) {
|
|
572
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
573
|
+
const preview = typeof response.Body === 'string'
|
|
574
|
+
? response.Body.slice(0, 500)
|
|
575
|
+
: JSON.stringify(response.Body).slice(0, 500);
|
|
576
|
+
throw new Error(`Graph HTTP ${response.Status} from ${url}: ${preview}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// ─── CRUD URL builder ────────────────────────────────────────────
|
|
580
|
+
/**
|
|
581
|
+
* Builds a CRUD-target URL for a given object + external ID.
|
|
582
|
+
*
|
|
583
|
+
* The APIPath from metadata may contain template variables (e.g.,
|
|
584
|
+
* `/sites/{siteId}/lists/{id}/items`). For a CRUD call we resolve those
|
|
585
|
+
* variables from the composite external ID: if the object has N template
|
|
586
|
+
* variables and an optional terminal ID, the external ID is a pipe-delimited
|
|
587
|
+
* string of N+1 parts (e.g., `siteId|listId|itemId`).
|
|
588
|
+
*/
|
|
589
|
+
BuildCRUDUrl(auth, integrationID, objectName, externalID) {
|
|
590
|
+
const obj = this.GetCachedObject(integrationID, objectName);
|
|
591
|
+
const apiPath = this.ResolveTemplatePath(obj.APIPath, externalID);
|
|
592
|
+
const base = auth.BaseUrl;
|
|
593
|
+
const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
594
|
+
return `${base}${path}`;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Resolves template variables in an API path using a pipe-delimited
|
|
598
|
+
* external ID (e.g., `site-id|list-id|item-id`).
|
|
599
|
+
*/
|
|
600
|
+
ResolveTemplatePath(apiPath, externalID) {
|
|
601
|
+
const templateVars = this.DetectTemplateVariables(apiPath);
|
|
602
|
+
if (templateVars.length === 0) {
|
|
603
|
+
// Flat path — append externalID if present
|
|
604
|
+
if (externalID)
|
|
605
|
+
return `${apiPath}/${encodeURIComponent(externalID)}`;
|
|
606
|
+
return apiPath;
|
|
607
|
+
}
|
|
608
|
+
if (!externalID)
|
|
609
|
+
return apiPath;
|
|
610
|
+
const parts = externalID.split('|');
|
|
611
|
+
let resolved = apiPath;
|
|
612
|
+
templateVars.forEach((varName, idx) => {
|
|
613
|
+
const value = parts[idx] ?? '';
|
|
614
|
+
resolved = resolved.replace(`{${varName}}`, encodeURIComponent(value));
|
|
615
|
+
});
|
|
616
|
+
// If there is an extra parts element beyond the template vars, treat it
|
|
617
|
+
// as the terminal ID (e.g., list item id)
|
|
618
|
+
if (parts.length > templateVars.length) {
|
|
619
|
+
const terminalId = parts[parts.length - 1];
|
|
620
|
+
resolved = `${resolved}/${encodeURIComponent(terminalId)}`;
|
|
621
|
+
}
|
|
622
|
+
return resolved;
|
|
623
|
+
}
|
|
624
|
+
DetectTemplateVariables(apiPath) {
|
|
625
|
+
const matches = apiPath.match(/\{(\w+)\}/g);
|
|
626
|
+
return matches ? matches.map(m => m.slice(1, -1)) : [];
|
|
627
|
+
}
|
|
628
|
+
// ─── Delta sync ──────────────────────────────────────────────────
|
|
629
|
+
/**
|
|
630
|
+
* Detects whether an IntegrationObject supports Graph delta queries.
|
|
631
|
+
* Currently: DriveItems and SiteListItems.
|
|
632
|
+
*/
|
|
633
|
+
SupportsDeltaForObject(obj) {
|
|
634
|
+
return obj.Name === 'DriveItems' || obj.Name === 'SiteListItems';
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Fetches changed records via Graph's /delta endpoint. The `WatermarkValue`
|
|
638
|
+
* holds either a full delta URL (from the previous batch) or a token to
|
|
639
|
+
* append to the delta endpoint.
|
|
640
|
+
*/
|
|
641
|
+
async FetchChangesViaDelta(ctx, obj) {
|
|
642
|
+
const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
|
|
643
|
+
const deltaUrl = this.BuildDeltaUrl(auth, obj, ctx.WatermarkValue);
|
|
644
|
+
const response = await this.MakeHTTPRequest(auth, deltaUrl, 'GET', this.BuildHeaders(auth));
|
|
645
|
+
this.ValidateResponse(response, deltaUrl);
|
|
646
|
+
const body = response.Body;
|
|
647
|
+
const records = body.value ?? [];
|
|
648
|
+
const nextLink = body['@odata.nextLink'];
|
|
649
|
+
const deltaLink = body['@odata.deltaLink'];
|
|
650
|
+
const externalRecords = records.map(r => ({
|
|
651
|
+
ExternalID: String(r['id'] ?? ''),
|
|
652
|
+
ObjectType: ctx.ObjectName,
|
|
653
|
+
Fields: r,
|
|
654
|
+
ModifiedAt: typeof r['lastModifiedDateTime'] === 'string'
|
|
655
|
+
? new Date(r['lastModifiedDateTime']) : undefined,
|
|
656
|
+
// Microsoft Graph delta deletion semantics: driveItem and listItem use the
|
|
657
|
+
// `deleted` facet (e.g. `"deleted": { "state": "deleted" }`). The legacy
|
|
658
|
+
// `@removed` shape applies to directoryObjects (users/groups) only. Check
|
|
659
|
+
// both so we catch deletions across the resource types this connector reaches.
|
|
660
|
+
IsDeleted: (typeof r['deleted'] === 'object' && r['deleted'] !== null) ||
|
|
661
|
+
(typeof r['@removed'] === 'object' && r['@removed'] !== null),
|
|
662
|
+
}));
|
|
663
|
+
return {
|
|
664
|
+
Records: externalRecords,
|
|
665
|
+
HasMore: Boolean(nextLink),
|
|
666
|
+
NewWatermarkValue: deltaLink ?? nextLink,
|
|
667
|
+
NextCursor: nextLink,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
BuildDeltaUrl(auth, obj, watermark) {
|
|
671
|
+
// If watermark is already a full URL, use it as-is
|
|
672
|
+
if (watermark && watermark.startsWith('http'))
|
|
673
|
+
return watermark;
|
|
674
|
+
// Translate object's APIPath into its /delta sibling
|
|
675
|
+
const base = auth.BaseUrl;
|
|
676
|
+
const path = obj.Name === 'DriveItems'
|
|
677
|
+
? '/drives/{id}/root/delta'
|
|
678
|
+
: '/sites/{siteId}/lists/{id}/items/delta';
|
|
679
|
+
const deltaBase = `${base}${path}`;
|
|
680
|
+
if (watermark)
|
|
681
|
+
return `${deltaBase}?token=${encodeURIComponent(watermark)}`;
|
|
682
|
+
return deltaBase;
|
|
683
|
+
}
|
|
684
|
+
// ─── Runtime List-Column Discovery (Custom Fields) ───────────────
|
|
685
|
+
/**
|
|
686
|
+
* Enumerates every accessible (site, list) pair and fetches each list's
|
|
687
|
+
* column definitions, returning a deduplicated array of fields not already
|
|
688
|
+
* present in the static metadata.
|
|
689
|
+
*
|
|
690
|
+
* Note: For large tenants this can be expensive. Callers typically scope
|
|
691
|
+
* this to specific sites via ExtraFilter / configuration.
|
|
692
|
+
*/
|
|
693
|
+
async DiscoverListColumnsForAllLists(companyIntegration, contextUser, staticFields) {
|
|
694
|
+
try {
|
|
695
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
696
|
+
const sites = await this.ListAllSites(auth);
|
|
697
|
+
const staticFieldNames = new Set(staticFields.map(f => f.Name.toLowerCase()));
|
|
698
|
+
const seen = new Set();
|
|
699
|
+
const results = [];
|
|
700
|
+
for (const site of sites) {
|
|
701
|
+
const lists = await this.ListListsForSite(auth, site.id);
|
|
702
|
+
for (const list of lists) {
|
|
703
|
+
const columns = await this.ListColumnsForList(auth, site.id, list.id);
|
|
704
|
+
for (const col of columns) {
|
|
705
|
+
const name = col.name ?? col.id;
|
|
706
|
+
if (!name)
|
|
707
|
+
continue;
|
|
708
|
+
const key = name.toLowerCase();
|
|
709
|
+
if (staticFieldNames.has(key) || seen.has(key))
|
|
710
|
+
continue;
|
|
711
|
+
seen.add(key);
|
|
712
|
+
results.push(this.GraphColumnToFieldSchema(col));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return results;
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// Best-effort — degrade gracefully if live discovery fails
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async ListAllSites(auth) {
|
|
724
|
+
const url = `${auth.BaseUrl}/sites?search=*&$select=id,displayName,name,webUrl`;
|
|
725
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
726
|
+
this.ValidateResponse(response, url);
|
|
727
|
+
const body = response.Body;
|
|
728
|
+
return body.value ?? [];
|
|
729
|
+
}
|
|
730
|
+
async ListListsForSite(auth, siteId) {
|
|
731
|
+
const url = `${auth.BaseUrl}/sites/${encodeURIComponent(siteId)}/lists?$select=id,name,displayName`;
|
|
732
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
733
|
+
if (response.Status >= 400)
|
|
734
|
+
return [];
|
|
735
|
+
const body = response.Body;
|
|
736
|
+
return body.value ?? [];
|
|
737
|
+
}
|
|
738
|
+
async ListColumnsForList(auth, siteId, listId) {
|
|
739
|
+
const url = `${auth.BaseUrl}/sites/${encodeURIComponent(siteId)}/lists/${encodeURIComponent(listId)}/columns`;
|
|
740
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
741
|
+
if (response.Status >= 400)
|
|
742
|
+
return [];
|
|
743
|
+
const body = response.Body;
|
|
744
|
+
return body.value ?? [];
|
|
745
|
+
}
|
|
746
|
+
GraphColumnToFieldSchema(col) {
|
|
747
|
+
return {
|
|
748
|
+
Name: col.name ?? col.id ?? 'unknown',
|
|
749
|
+
Label: col.displayName ?? col.name ?? col.id ?? 'unknown',
|
|
750
|
+
Description: col.description ?? undefined,
|
|
751
|
+
DataType: this.InferGraphColumnType(col),
|
|
752
|
+
IsRequired: col.required === true,
|
|
753
|
+
IsUniqueKey: false,
|
|
754
|
+
IsReadOnly: col.readOnly === true,
|
|
755
|
+
IsForeignKey: false,
|
|
756
|
+
ForeignKeyTarget: null,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
InferGraphColumnType(col) {
|
|
760
|
+
if (col.text)
|
|
761
|
+
return 'string';
|
|
762
|
+
if (col.number)
|
|
763
|
+
return 'decimal';
|
|
764
|
+
if (col.boolean)
|
|
765
|
+
return 'boolean';
|
|
766
|
+
if (col.dateTime)
|
|
767
|
+
return 'datetime';
|
|
768
|
+
if (col.choice)
|
|
769
|
+
return 'string';
|
|
770
|
+
if (col.currency)
|
|
771
|
+
return 'decimal';
|
|
772
|
+
if (col.lookup)
|
|
773
|
+
return 'string';
|
|
774
|
+
if (col.personOrGroup)
|
|
775
|
+
return 'string';
|
|
776
|
+
if (col.hyperlinkOrPicture)
|
|
777
|
+
return 'string';
|
|
778
|
+
return 'string';
|
|
779
|
+
}
|
|
780
|
+
// ─── CRUD helpers ────────────────────────────────────────────────
|
|
781
|
+
BuildCRUDError(err, operation, objectName) {
|
|
782
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
783
|
+
return {
|
|
784
|
+
Success: false,
|
|
785
|
+
ErrorMessage: `${operation} failed for ${objectName}: ${message}`,
|
|
786
|
+
StatusCode: 500,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
SharePointConnector = __decorate([
|
|
791
|
+
RegisterClass(BaseIntegrationConnector, 'SharePointConnector')
|
|
792
|
+
], SharePointConnector);
|
|
793
|
+
export { SharePointConnector };
|
|
794
|
+
/** Tree-shaking prevention function — import and call from module entry point. */
|
|
795
|
+
export function LoadSharePointConnector() { }
|
|
796
|
+
//# sourceMappingURL=SharePointConnector.js.map
|