@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.
Files changed (92) hide show
  1. package/dist/CventConnector.d.ts +146 -0
  2. package/dist/CventConnector.d.ts.map +1 -0
  3. package/dist/CventConnector.js +540 -0
  4. package/dist/CventConnector.js.map +1 -0
  5. package/dist/DynamicsDataverseConnector.d.ts +296 -0
  6. package/dist/DynamicsDataverseConnector.d.ts.map +1 -0
  7. package/dist/DynamicsDataverseConnector.js +790 -0
  8. package/dist/DynamicsDataverseConnector.js.map +1 -0
  9. package/dist/FontevaConnector.d.ts +153 -0
  10. package/dist/FontevaConnector.d.ts.map +1 -0
  11. package/dist/FontevaConnector.js +421 -0
  12. package/dist/FontevaConnector.js.map +1 -0
  13. package/dist/GrowthZoneConnector.d.ts +123 -125
  14. package/dist/GrowthZoneConnector.d.ts.map +1 -1
  15. package/dist/GrowthZoneConnector.js +456 -499
  16. package/dist/GrowthZoneConnector.js.map +1 -1
  17. package/dist/HivebriteConnector.d.ts +151 -0
  18. package/dist/HivebriteConnector.d.ts.map +1 -0
  19. package/dist/HivebriteConnector.js +533 -0
  20. package/dist/HivebriteConnector.js.map +1 -0
  21. package/dist/IMISConnector.d.ts +112 -96
  22. package/dist/IMISConnector.d.ts.map +1 -1
  23. package/dist/IMISConnector.js +430 -417
  24. package/dist/IMISConnector.js.map +1 -1
  25. package/dist/MemberSuiteConnector.d.ts +185 -0
  26. package/dist/MemberSuiteConnector.d.ts.map +1 -0
  27. package/dist/MemberSuiteConnector.js +762 -0
  28. package/dist/MemberSuiteConnector.js.map +1 -0
  29. package/dist/NeonCRMConnector.d.ts +273 -0
  30. package/dist/NeonCRMConnector.d.ts.map +1 -0
  31. package/dist/NeonCRMConnector.js +1039 -0
  32. package/dist/NeonCRMConnector.js.map +1 -0
  33. package/dist/NetForumConnector.d.ts +140 -47
  34. package/dist/NetForumConnector.d.ts.map +1 -1
  35. package/dist/NetForumConnector.js +668 -395
  36. package/dist/NetForumConnector.js.map +1 -1
  37. package/dist/NetSuiteConnector.d.ts +116 -50
  38. package/dist/NetSuiteConnector.d.ts.map +1 -1
  39. package/dist/NetSuiteConnector.js +608 -510
  40. package/dist/NetSuiteConnector.js.map +1 -1
  41. package/dist/NimbleAMSConnector.d.ts +104 -11
  42. package/dist/NimbleAMSConnector.d.ts.map +1 -1
  43. package/dist/NimbleAMSConnector.js +552 -258
  44. package/dist/NimbleAMSConnector.js.map +1 -1
  45. package/dist/NoviConnector.d.ts +178 -0
  46. package/dist/NoviConnector.d.ts.map +1 -0
  47. package/dist/NoviConnector.js +566 -0
  48. package/dist/NoviConnector.js.map +1 -0
  49. package/dist/ORCIDConnector.d.ts +207 -0
  50. package/dist/ORCIDConnector.d.ts.map +1 -0
  51. package/dist/ORCIDConnector.js +708 -0
  52. package/dist/ORCIDConnector.js.map +1 -0
  53. package/dist/OpenWaterConnector.d.ts +173 -0
  54. package/dist/OpenWaterConnector.d.ts.map +1 -0
  55. package/dist/OpenWaterConnector.js +893 -0
  56. package/dist/OpenWaterConnector.js.map +1 -0
  57. package/dist/PathLMSConnector.d.ts +395 -0
  58. package/dist/PathLMSConnector.d.ts.map +1 -0
  59. package/dist/PathLMSConnector.js +1281 -0
  60. package/dist/PathLMSConnector.js.map +1 -0
  61. package/dist/PheedLoopConnector.d.ts +119 -0
  62. package/dist/PheedLoopConnector.d.ts.map +1 -0
  63. package/dist/PheedLoopConnector.js +429 -0
  64. package/dist/PheedLoopConnector.js.map +1 -0
  65. package/dist/PropFuelConnector.d.ts +135 -61
  66. package/dist/PropFuelConnector.d.ts.map +1 -1
  67. package/dist/PropFuelConnector.js +366 -467
  68. package/dist/PropFuelConnector.js.map +1 -1
  69. package/dist/RhythmConnector.d.ts +188 -0
  70. package/dist/RhythmConnector.d.ts.map +1 -0
  71. package/dist/RhythmConnector.js +784 -0
  72. package/dist/RhythmConnector.js.map +1 -0
  73. package/dist/SalesforceConnector.d.ts +69 -3
  74. package/dist/SalesforceConnector.d.ts.map +1 -1
  75. package/dist/SalesforceConnector.js +188 -166
  76. package/dist/SalesforceConnector.js.map +1 -1
  77. package/dist/SharePointConnector.d.ts +37 -30
  78. package/dist/SharePointConnector.d.ts.map +1 -1
  79. package/dist/SharePointConnector.js +163 -243
  80. package/dist/SharePointConnector.js.map +1 -1
  81. package/dist/YourMembershipConnector.d.ts.map +1 -1
  82. package/dist/YourMembershipConnector.js +13 -5
  83. package/dist/YourMembershipConnector.js.map +1 -1
  84. package/dist/index.d.ts +14 -3
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +14 -3
  87. package/dist/index.js.map +1 -1
  88. package/package.json +8 -7
  89. package/dist/BettyConnector.d.ts +0 -288
  90. package/dist/BettyConnector.d.ts.map +0 -1
  91. package/dist/BettyConnector.js +0 -909
  92. 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
- /** 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
- ];
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?token=...` endpoints for drives and list items.
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
- get SupportsCreate() { return true; }
125
- get SupportsUpdate() { return true; }
126
- get SupportsDelete() { return true; }
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
- 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
- }
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: 'Sites',
121
+ SourceObjectName: 'Site',
154
122
  TargetTableName: 'SharePoint_Site',
155
123
  TargetEntityName: 'SharePoint Sites',
156
124
  SyncEnabled: true,
157
- FieldMappings: this.GetDefaultFieldMappings('Sites', 'Contacts'),
125
+ FieldMappings: this.GetDefaultFieldMappings('Site', 'Sites'),
158
126
  },
159
127
  {
160
- SourceObjectName: 'SiteLists',
128
+ SourceObjectName: 'List',
161
129
  TargetTableName: 'SharePoint_List',
162
130
  TargetEntityName: 'SharePoint Lists',
163
131
  SyncEnabled: true,
164
- FieldMappings: this.GetDefaultFieldMappings('SiteLists', 'Lists'),
132
+ FieldMappings: this.GetDefaultFieldMappings('List', 'Lists'),
165
133
  },
166
134
  {
167
- SourceObjectName: 'SiteListItems',
135
+ SourceObjectName: 'ListItem',
168
136
  TargetTableName: 'SharePoint_ListItem',
169
137
  TargetEntityName: 'SharePoint List Items',
170
138
  SyncEnabled: true,
171
- FieldMappings: this.GetDefaultFieldMappings('SiteListItems', 'ListItems'),
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 'Sites':
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 'SiteLists':
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 'SiteListItems':
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 present and object supports it,
255
- // use /delta endpoints rather than full re-enumeration.
256
- if (ctx.WatermarkValue && obj.SupportsIncrementalSync && this.SupportsDeltaForObject(obj)) {
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
- 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 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 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 };
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
- 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;
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
- * Currently: DriveItems and SiteListItems.
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 obj.Name === 'DriveItems' || obj.Name === 'SiteListItems';
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(auth, obj, watermark) {
671
- // If watermark is already a full URL, use it as-is
672
- if (watermark && watermark.startsWith('http'))
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
- // 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;
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')