@memberjunction/integration-connectors 5.41.0 → 5.43.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/CventConnector.d.ts +146 -0
- package/dist/CventConnector.d.ts.map +1 -0
- package/dist/CventConnector.js +540 -0
- package/dist/CventConnector.js.map +1 -0
- package/dist/DynamicsDataverseConnector.d.ts +296 -0
- package/dist/DynamicsDataverseConnector.d.ts.map +1 -0
- package/dist/DynamicsDataverseConnector.js +790 -0
- package/dist/DynamicsDataverseConnector.js.map +1 -0
- package/dist/FontevaConnector.d.ts +153 -0
- package/dist/FontevaConnector.d.ts.map +1 -0
- package/dist/FontevaConnector.js +421 -0
- package/dist/FontevaConnector.js.map +1 -0
- package/dist/GrowthZoneConnector.d.ts +123 -125
- package/dist/GrowthZoneConnector.d.ts.map +1 -1
- package/dist/GrowthZoneConnector.js +456 -499
- package/dist/GrowthZoneConnector.js.map +1 -1
- package/dist/HivebriteConnector.d.ts +151 -0
- package/dist/HivebriteConnector.d.ts.map +1 -0
- package/dist/HivebriteConnector.js +533 -0
- package/dist/HivebriteConnector.js.map +1 -0
- package/dist/IMISConnector.d.ts +112 -96
- package/dist/IMISConnector.d.ts.map +1 -1
- package/dist/IMISConnector.js +430 -417
- package/dist/IMISConnector.js.map +1 -1
- package/dist/MemberSuiteConnector.d.ts +185 -0
- package/dist/MemberSuiteConnector.d.ts.map +1 -0
- package/dist/MemberSuiteConnector.js +762 -0
- package/dist/MemberSuiteConnector.js.map +1 -0
- package/dist/NeonCRMConnector.d.ts +273 -0
- package/dist/NeonCRMConnector.d.ts.map +1 -0
- package/dist/NeonCRMConnector.js +1039 -0
- package/dist/NeonCRMConnector.js.map +1 -0
- package/dist/NetForumConnector.d.ts +140 -47
- package/dist/NetForumConnector.d.ts.map +1 -1
- package/dist/NetForumConnector.js +668 -395
- package/dist/NetForumConnector.js.map +1 -1
- package/dist/NetSuiteConnector.d.ts +116 -50
- package/dist/NetSuiteConnector.d.ts.map +1 -1
- package/dist/NetSuiteConnector.js +608 -510
- package/dist/NetSuiteConnector.js.map +1 -1
- package/dist/NimbleAMSConnector.d.ts +104 -11
- package/dist/NimbleAMSConnector.d.ts.map +1 -1
- package/dist/NimbleAMSConnector.js +552 -258
- package/dist/NimbleAMSConnector.js.map +1 -1
- package/dist/NoviConnector.d.ts +178 -0
- package/dist/NoviConnector.d.ts.map +1 -0
- package/dist/NoviConnector.js +566 -0
- package/dist/NoviConnector.js.map +1 -0
- package/dist/ORCIDConnector.d.ts +207 -0
- package/dist/ORCIDConnector.d.ts.map +1 -0
- package/dist/ORCIDConnector.js +708 -0
- package/dist/ORCIDConnector.js.map +1 -0
- package/dist/OpenWaterConnector.d.ts +173 -0
- package/dist/OpenWaterConnector.d.ts.map +1 -0
- package/dist/OpenWaterConnector.js +893 -0
- package/dist/OpenWaterConnector.js.map +1 -0
- package/dist/PathLMSConnector.d.ts +395 -0
- package/dist/PathLMSConnector.d.ts.map +1 -0
- package/dist/PathLMSConnector.js +1281 -0
- package/dist/PathLMSConnector.js.map +1 -0
- package/dist/PheedLoopConnector.d.ts +119 -0
- package/dist/PheedLoopConnector.d.ts.map +1 -0
- package/dist/PheedLoopConnector.js +429 -0
- package/dist/PheedLoopConnector.js.map +1 -0
- package/dist/PropFuelConnector.d.ts +135 -61
- package/dist/PropFuelConnector.d.ts.map +1 -1
- package/dist/PropFuelConnector.js +366 -467
- package/dist/PropFuelConnector.js.map +1 -1
- package/dist/RhythmConnector.d.ts +188 -0
- package/dist/RhythmConnector.d.ts.map +1 -0
- package/dist/RhythmConnector.js +784 -0
- package/dist/RhythmConnector.js.map +1 -0
- package/dist/SalesforceConnector.d.ts +69 -3
- package/dist/SalesforceConnector.d.ts.map +1 -1
- package/dist/SalesforceConnector.js +188 -166
- package/dist/SalesforceConnector.js.map +1 -1
- package/dist/SharePointConnector.d.ts +37 -30
- package/dist/SharePointConnector.d.ts.map +1 -1
- package/dist/SharePointConnector.js +163 -243
- package/dist/SharePointConnector.js.map +1 -1
- package/dist/YourMembershipConnector.d.ts.map +1 -1
- package/dist/YourMembershipConnector.js +13 -5
- package/dist/YourMembershipConnector.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/dist/BettyConnector.d.ts +0 -288
- package/dist/BettyConnector.d.ts.map +0 -1
- package/dist/BettyConnector.js +0 -909
- package/dist/BettyConnector.js.map +0 -1
|
@@ -22,78 +22,30 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
|
22
22
|
const DEFAULT_MAX_RETRIES = 5;
|
|
23
23
|
/** Buffer before token expiry at which to refresh (60s). */
|
|
24
24
|
const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
];
|
|
25
|
+
/**
|
|
26
|
+
* IntegrationObject name whose runtime field discovery augments static metadata
|
|
27
|
+
* with per-list column definitions. Matches the frozen-contract object name `ListItem`.
|
|
28
|
+
*/
|
|
29
|
+
const LIST_ITEM_OBJECT = 'ListItem';
|
|
30
|
+
/**
|
|
31
|
+
* Object names that support Microsoft Graph `/delta` incremental queries.
|
|
32
|
+
* Per the re-derived contract, exactly Site, DriveItem, and ListItem carry
|
|
33
|
+
* `IncrementalWatermarkField=@odata.deltaLink`; everything else is full-scan.
|
|
34
|
+
*/
|
|
35
|
+
const DELTA_OBJECTS = new Set(['Site', 'DriveItem', 'ListItem']);
|
|
36
|
+
/**
|
|
37
|
+
* Objects whose Microsoft Graph CREATE shape is genuinely idiosyncratic and cannot be
|
|
38
|
+
* expressed by the generic flat/wrapped BodyShape columns, so CreateRecord is overridden
|
|
39
|
+
* for them (and ONLY them). Everything else uses the generic per-operation CRUD path.
|
|
40
|
+
*
|
|
41
|
+
* - ListItem: `POST /sites/{site}/lists/{list}/items` requires the column values nested
|
|
42
|
+
* under a `fields` envelope: `{ "fields": { Title: "x", ... } }`. The metadata declares
|
|
43
|
+
* `CreateBodyShape=flat` (because the UPDATE path `/items/{id}/fields` PATCHes the flat
|
|
44
|
+
* map directly — create and update are asymmetric in Graph), so the generic flat create
|
|
45
|
+
* would POST the bare map and Graph rejects it. We re-wrap here. The override STILL routes
|
|
46
|
+
* through BuildCreatedResult so a 2xx-with-no-id fails loudly.
|
|
47
|
+
*/
|
|
48
|
+
const LISTITEM_CREATE_OBJECT = 'ListItem';
|
|
97
49
|
// ─── Connector Implementation ────────────────────────────────────────
|
|
98
50
|
/**
|
|
99
51
|
* Connector for Microsoft SharePoint Online via Microsoft Graph v1.0.
|
|
@@ -108,9 +60,15 @@ const SHAREPOINT_OBJECTS = [
|
|
|
108
60
|
* 2. Bearer token attached to every Graph request
|
|
109
61
|
* 3. Token is refreshed proactively before expiry
|
|
110
62
|
*
|
|
111
|
-
* Pagination: `@odata.nextLink` cursor (per OData convention).
|
|
63
|
+
* Pagination: `@odata.nextLink` cursor (per OData convention) — replayed verbatim.
|
|
112
64
|
*
|
|
113
|
-
* Incremental sync: `/delta
|
|
65
|
+
* Incremental sync: Graph `/delta` for Site, DriveItem, and ListItem (the three
|
|
66
|
+
* objects the frozen contract proves carry `@odata.deltaLink`). The watermark is
|
|
67
|
+
* the full deltaLink URL, replayed verbatim; the first sync runs as a full fetch.
|
|
68
|
+
*
|
|
69
|
+
* Discovery is non-authoritative (DiscoveryIsAuthoritative inherits false): the 25
|
|
70
|
+
* standard objects come from Declared metadata; runtime field discovery only ADDS
|
|
71
|
+
* tenant-specific list columns. Absence in a refresh never deactivates.
|
|
114
72
|
*/
|
|
115
73
|
let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationConnector {
|
|
116
74
|
constructor() {
|
|
@@ -121,74 +79,84 @@ let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationC
|
|
|
121
79
|
this.lastRequestTime = 0;
|
|
122
80
|
}
|
|
123
81
|
// ── Capability Getters ───────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
82
|
+
// RUNTIME STATUS: this connector is READ-ONLY end-to-end. The global
|
|
83
|
+
// SupportsCreate/Update/Delete getters inherit `false`, and BOTH the
|
|
84
|
+
// IntegrationEngine push path AND the IntegrationWriteRecord resolver gate every
|
|
85
|
+
// write on those GLOBAL getters — so no write executes through the product today,
|
|
86
|
+
// regardless of the per-object metadata. Read sync is what is live-proven.
|
|
87
|
+
//
|
|
88
|
+
// The write surface below (the ListItem CreateRecord override + the Create/Update/
|
|
89
|
+
// Delete columns the frozen contract populates for DriveItem / List / ListItem /
|
|
90
|
+
// Subscription) is implemented and unit-tested, but is intentionally PER-OBJECT, not
|
|
91
|
+
// global: we do NOT flip the global getters to `true`, because that would wrongly
|
|
92
|
+
// advertise writes for Site / Drive / the other read-only objects. It activates only
|
|
93
|
+
// once the engine adds per-object write dispatch (a separate framework change); until
|
|
94
|
+
// then it is dormant by design, and the base generic CRUD throws a clear
|
|
95
|
+
// "not configured" error for any object whose per-operation columns are null.
|
|
96
|
+
/**
|
|
97
|
+
* Verbatim from the frozen contract's Integration.Name AND the baseline-seeded
|
|
98
|
+
* `__mj.Integration` row — both are exactly `'SharePoint'`. This is load-bearing:
|
|
99
|
+
* the three-way invariant `IntegrationName === MJ: Integrations.Name === @RegisterClass
|
|
100
|
+
* driver→ClassName` is how the engine binds this connector to its Integration row. A
|
|
101
|
+
* mismatch (e.g. 'SharePoint Online') means the engine never resolves the connector,
|
|
102
|
+
* no connection is created, and the live sync lands 0 rows. DO NOT change this string.
|
|
103
|
+
*/
|
|
127
104
|
get IntegrationName() { return 'SharePoint'; }
|
|
128
105
|
// ── Action Generation ────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
}
|
|
106
|
+
//
|
|
107
|
+
// We deliberately do NOT override GetIntegrationObjects / GetActionGeneratorConfig.
|
|
108
|
+
// The object/field catalog is NOT baked in connector code — it lives entirely in the
|
|
109
|
+
// 25 Declared IntegrationObject rows in metadata (case 1: credential-free Graph spec).
|
|
110
|
+
// Baking a module-level catalog constant here is the `catalog-in-code` floor-check
|
|
111
|
+
// failure (it freezes the object set AND becomes a circular source a later build reads
|
|
112
|
+
// its own output back from). The base GetIntegrationObjects() returns [] and the base
|
|
113
|
+
// GetActionGeneratorConfig() returns null when objects are empty — exactly what we want.
|
|
114
|
+
// DiscoverObjects (below) delegates to super, which reads the Declared rows at runtime.
|
|
147
115
|
// ── Default Configuration ────────────────────────────────────────
|
|
148
116
|
GetDefaultConfiguration() {
|
|
149
117
|
return {
|
|
150
118
|
DefaultSchemaName: 'SharePoint',
|
|
151
119
|
DefaultObjects: [
|
|
152
120
|
{
|
|
153
|
-
SourceObjectName: '
|
|
121
|
+
SourceObjectName: 'Site',
|
|
154
122
|
TargetTableName: 'SharePoint_Site',
|
|
155
123
|
TargetEntityName: 'SharePoint Sites',
|
|
156
124
|
SyncEnabled: true,
|
|
157
|
-
FieldMappings: this.GetDefaultFieldMappings('
|
|
125
|
+
FieldMappings: this.GetDefaultFieldMappings('Site', 'Sites'),
|
|
158
126
|
},
|
|
159
127
|
{
|
|
160
|
-
SourceObjectName: '
|
|
128
|
+
SourceObjectName: 'List',
|
|
161
129
|
TargetTableName: 'SharePoint_List',
|
|
162
130
|
TargetEntityName: 'SharePoint Lists',
|
|
163
131
|
SyncEnabled: true,
|
|
164
|
-
FieldMappings: this.GetDefaultFieldMappings('
|
|
132
|
+
FieldMappings: this.GetDefaultFieldMappings('List', 'Lists'),
|
|
165
133
|
},
|
|
166
134
|
{
|
|
167
|
-
SourceObjectName: '
|
|
135
|
+
SourceObjectName: 'ListItem',
|
|
168
136
|
TargetTableName: 'SharePoint_ListItem',
|
|
169
137
|
TargetEntityName: 'SharePoint List Items',
|
|
170
138
|
SyncEnabled: true,
|
|
171
|
-
FieldMappings: this.GetDefaultFieldMappings('
|
|
139
|
+
FieldMappings: this.GetDefaultFieldMappings('ListItem', 'ListItems'),
|
|
172
140
|
},
|
|
173
141
|
],
|
|
174
142
|
};
|
|
175
143
|
}
|
|
176
144
|
GetDefaultFieldMappings(objectName, _entityName) {
|
|
177
145
|
switch (objectName) {
|
|
178
|
-
case '
|
|
146
|
+
case 'Site':
|
|
179
147
|
return [
|
|
180
148
|
{ SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
181
149
|
{ SourceFieldName: 'displayName', DestinationFieldName: 'Name' },
|
|
182
150
|
{ SourceFieldName: 'webUrl', DestinationFieldName: 'URL' },
|
|
183
151
|
{ SourceFieldName: 'description', DestinationFieldName: 'Description' },
|
|
184
152
|
];
|
|
185
|
-
case '
|
|
153
|
+
case 'List':
|
|
186
154
|
return [
|
|
187
155
|
{ SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
188
156
|
{ SourceFieldName: 'displayName', DestinationFieldName: 'Name' },
|
|
189
157
|
{ SourceFieldName: 'description', DestinationFieldName: 'Description' },
|
|
190
158
|
];
|
|
191
|
-
case '
|
|
159
|
+
case 'ListItem':
|
|
192
160
|
return [
|
|
193
161
|
{ SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
194
162
|
];
|
|
@@ -251,80 +219,73 @@ let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationC
|
|
|
251
219
|
*/
|
|
252
220
|
async FetchChanges(ctx) {
|
|
253
221
|
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
254
|
-
// Delta sync path — when watermark is
|
|
255
|
-
//
|
|
256
|
-
|
|
222
|
+
// Delta sync path — only when the watermark is a full, absolute Graph delta
|
|
223
|
+
// URL (the verbatim @odata.deltaLink from a previous batch). The first sync
|
|
224
|
+
// has no delta link yet, so it falls through to the base full fetch, which
|
|
225
|
+
// does the parent-chain template traversal and yields the deltaLink for next
|
|
226
|
+
// time. We never synthesise a delta URL from a bare token.
|
|
227
|
+
const wm = ctx.WatermarkValue;
|
|
228
|
+
if (wm && /^https?:\/\//.test(wm) && obj.SupportsIncrementalSync && this.SupportsDeltaForObject(obj)) {
|
|
257
229
|
return this.FetchChangesViaDelta(ctx, obj);
|
|
258
230
|
}
|
|
259
231
|
return super.FetchChanges(ctx);
|
|
260
232
|
}
|
|
261
233
|
// ── CRUD operations ──────────────────────────────────────────────
|
|
234
|
+
//
|
|
235
|
+
// Update / Delete / Get use the GENERIC BaseRESTIntegrationConnector path verbatim:
|
|
236
|
+
// the frozen contract fully populates the per-operation columns for the writable IOs
|
|
237
|
+
// (DriveItem / List / ListItem / Subscription) — Update/DeleteAPIPath/Method/BodyShape/
|
|
238
|
+
// IDLocation — including ListItem's Graph-specific `/items/{id}/fields` PATCH path,
|
|
239
|
+
// whose flat body the generic `flat` shape sends correctly. We do NOT override those.
|
|
240
|
+
//
|
|
241
|
+
// Create is overridden for EXACTLY ONE object — ListItem — because Graph's
|
|
242
|
+
// `POST .../items` is the one create whose envelope (`{ "fields": {...} }`) cannot be
|
|
243
|
+
// expressed by the flat/wrapped columns (create is asymmetric to the flat `/fields`
|
|
244
|
+
// PATCH update). Every other object's create falls straight through to the generic
|
|
245
|
+
// path; the ListItem branch STILL routes through BuildCreatedResult so a 2xx with no
|
|
246
|
+
// ID fails loudly (the HubSpot silent-loss guard).
|
|
247
|
+
//
|
|
248
|
+
// (DriveItem *content* upload — Graph's `createUploadSession` chunked PUT — is a
|
|
249
|
+
// separate, multi-step write that is NOT part of the frozen contract's CRUD surface;
|
|
250
|
+
// DriveItem create here is the metadata POST that the generic path handles. If/when
|
|
251
|
+
// content upload is added it belongs in its own override, also via BuildCreatedResult.)
|
|
252
|
+
/**
|
|
253
|
+
* Generic create for every object EXCEPT ListItem, whose Graph create requires the
|
|
254
|
+
* `{ "fields": {...} }` envelope. We re-wrap the attributes for ListItem and execute
|
|
255
|
+
* the same generic mechanics (auth → headers → POST → BuildCreatedResult), so the
|
|
256
|
+
* loud-on-empty-ID guarantee is preserved.
|
|
257
|
+
*/
|
|
262
258
|
async CreateRecord(ctx) {
|
|
263
|
-
|
|
264
|
-
|
|
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 this.BuildCreatedResult(body.id, response.Status, ctx.ObjectName);
|
|
272
|
-
}
|
|
273
|
-
catch (err) {
|
|
274
|
-
return this.BuildCRUDError(err, 'CreateRecord', ctx.ObjectName);
|
|
259
|
+
if (ctx.ObjectName !== LISTITEM_CREATE_OBJECT) {
|
|
260
|
+
return super.CreateRecord(ctx);
|
|
275
261
|
}
|
|
276
|
-
|
|
277
|
-
async UpdateRecord(ctx) {
|
|
278
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
262
|
+
const ci = ctx.CompanyIntegration;
|
|
279
263
|
const contextUser = ctx.ContextUser;
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
this.ValidateResponse(response, url);
|
|
285
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
264
|
+
const obj = this.GetCachedObject(ci.IntegrationID, ctx.ObjectName);
|
|
265
|
+
if (!obj.CreateAPIPath || !obj.CreateMethod) {
|
|
266
|
+
throw new Error(`CreateRecord not supported for "${ctx.ObjectName}": ` +
|
|
267
|
+
`CreateAPIPath / CreateMethod not configured on IntegrationObject.`);
|
|
286
268
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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;
|
|
269
|
+
const auth = await this.Authenticate(ci, contextUser);
|
|
270
|
+
const headers = this.BuildHeaders(auth);
|
|
271
|
+
const baseURL = this.GetBaseURL(ci, auth);
|
|
272
|
+
const url = `${baseURL.replace(/\/+$/, '')}${obj.CreateAPIPath.startsWith('/') ? '' : '/'}${obj.CreateAPIPath}`;
|
|
273
|
+
// The one idiosyncrasy: Graph wants the column map under `fields`. If the caller
|
|
274
|
+
// already nested it (idempotent), don't double-wrap.
|
|
275
|
+
const attrs = ctx.Attributes;
|
|
276
|
+
const body = ('fields' in attrs && typeof attrs.fields === 'object')
|
|
277
|
+
? attrs
|
|
278
|
+
: { fields: attrs };
|
|
279
|
+
const response = await this.MakeHTTPRequest(auth, url, obj.CreateMethod, headers, body);
|
|
280
|
+
if (response.Status >= 200 && response.Status < 300) {
|
|
281
|
+
const externalID = this.ExtractIDFromResponse(response, obj.CreateIDLocation);
|
|
282
|
+
return this.BuildCreatedResult(externalID, response.Status, ctx.ObjectName);
|
|
327
283
|
}
|
|
284
|
+
return {
|
|
285
|
+
Success: false,
|
|
286
|
+
StatusCode: response.Status,
|
|
287
|
+
ErrorMessage: this.ExtractErrorMessage(response) ?? `HTTP ${response.Status} on create`,
|
|
288
|
+
};
|
|
328
289
|
}
|
|
329
290
|
// ─── Abstract BaseRESTIntegrationConnector hooks ────────────────
|
|
330
291
|
/**
|
|
@@ -383,15 +344,32 @@ let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationC
|
|
|
383
344
|
* already passes `ResponseDataKey` from metadata — we default to the `value`
|
|
384
345
|
* convention when responseDataKey is null.
|
|
385
346
|
*/
|
|
347
|
+
/**
|
|
348
|
+
* Scope filter: a SharePoint *document-library* connector enumerates SharePoint sites
|
|
349
|
+
* (team/communication sites under `<tenant>.sharepoint.com/sites/...`), NOT users' personal
|
|
350
|
+
* OneDrive sites (`<tenant>-my.sharepoint.com/personal/...`). Personal sites are a different
|
|
351
|
+
* product (OneDrive), are routinely admin-locked (Graph returns HTTP 423 `resourceLocked` on
|
|
352
|
+
* their `/drives`), and carry no SharePoint document libraries — so iterating Drive/DriveItem
|
|
353
|
+
* over them yields only errors and starves the traversal before it reaches real document sites.
|
|
354
|
+
* We drop them at the Site source so they never become parents for the second-layer objects.
|
|
355
|
+
* Only Site records carry a `webUrl` on the `-my` host, so this is a no-op for every other object.
|
|
356
|
+
*/
|
|
357
|
+
ExcludePersonalSites(records) {
|
|
358
|
+
return records.filter(r => {
|
|
359
|
+
const webUrl = typeof r.webUrl === 'string' ? r.webUrl : '';
|
|
360
|
+
const isPersonal = r.isPersonalSite === true || webUrl.includes('-my.sharepoint.com');
|
|
361
|
+
return !isPersonal;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
386
364
|
NormalizeResponse(rawBody, responseDataKey) {
|
|
387
365
|
const body = rawBody;
|
|
388
366
|
const key = responseDataKey ?? 'value';
|
|
389
367
|
const data = body[key];
|
|
390
368
|
if (Array.isArray(data))
|
|
391
|
-
return data;
|
|
369
|
+
return this.ExcludePersonalSites(data);
|
|
392
370
|
// Single-record response fallback
|
|
393
371
|
if (body && typeof body === 'object' && 'id' in body)
|
|
394
|
-
return [body];
|
|
372
|
+
return this.ExcludePersonalSites([body]);
|
|
395
373
|
return [];
|
|
396
374
|
}
|
|
397
375
|
/**
|
|
@@ -576,62 +554,14 @@ let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationC
|
|
|
576
554
|
throw new Error(`Graph HTTP ${response.Status} from ${url}: ${preview}`);
|
|
577
555
|
}
|
|
578
556
|
}
|
|
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
557
|
// ─── Delta sync ──────────────────────────────────────────────────
|
|
629
558
|
/**
|
|
630
|
-
* Detects whether an IntegrationObject supports Graph delta queries.
|
|
631
|
-
*
|
|
559
|
+
* Detects whether an IntegrationObject supports Graph `/delta` queries.
|
|
560
|
+
* Per the frozen contract, exactly Site, DriveItem, and ListItem carry
|
|
561
|
+
* `IncrementalWatermarkField=@odata.deltaLink`.
|
|
632
562
|
*/
|
|
633
563
|
SupportsDeltaForObject(obj) {
|
|
634
|
-
return
|
|
564
|
+
return DELTA_OBJECTS.has(obj.Name);
|
|
635
565
|
}
|
|
636
566
|
/**
|
|
637
567
|
* Fetches changed records via Graph's /delta endpoint. The `WatermarkValue`
|
|
@@ -667,19 +597,18 @@ let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationC
|
|
|
667
597
|
NextCursor: nextLink,
|
|
668
598
|
};
|
|
669
599
|
}
|
|
670
|
-
BuildDeltaUrl(
|
|
671
|
-
//
|
|
672
|
-
|
|
600
|
+
BuildDeltaUrl(_auth, _obj, watermark) {
|
|
601
|
+
// The watermark IS the verbatim `@odata.deltaLink` (or `@odata.nextLink`) URL
|
|
602
|
+
// returned by Graph on the previous batch — a complete, absolute continuation
|
|
603
|
+
// request. We never reconstruct it from a token: Graph's delta/next links are
|
|
604
|
+
// opaque and must be replayed exactly as returned (the same VERBATIM rule as
|
|
605
|
+
// ExtractPaginationInfo's @odata.nextLink handling).
|
|
606
|
+
if (watermark && /^https?:\/\//.test(watermark))
|
|
673
607
|
return watermark;
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
: '/sites/{siteId}/lists/{id}/items/delta';
|
|
679
|
-
const deltaBase = `${base}${path}`;
|
|
680
|
-
if (watermark)
|
|
681
|
-
return `${deltaBase}?token=${encodeURIComponent(watermark)}`;
|
|
682
|
-
return deltaBase;
|
|
608
|
+
// No usable delta link yet (first sync) — caller gates on this; we should not
|
|
609
|
+
// be here without an absolute delta URL.
|
|
610
|
+
throw new Error(`SharePointConnector: delta sync for "${_obj.Name}" requires a full @odata.deltaLink ` +
|
|
611
|
+
`watermark; got "${watermark ?? 'null'}". First-time sync must run via full fetch.`);
|
|
683
612
|
}
|
|
684
613
|
// ─── Runtime List-Column Discovery (Custom Fields) ───────────────
|
|
685
614
|
/**
|
|
@@ -777,15 +706,6 @@ let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationC
|
|
|
777
706
|
return 'string';
|
|
778
707
|
return 'string';
|
|
779
708
|
}
|
|
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
709
|
};
|
|
790
710
|
SharePointConnector = __decorate([
|
|
791
711
|
RegisterClass(BaseIntegrationConnector, 'SharePointConnector')
|