@memberjunction/integration-connectors 5.50.0 → 6.1.0-edge.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +109 -277
  3. package/dist/index.d.ts +0 -36
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +7 -38
  6. package/dist/index.js.map +1 -1
  7. package/package.json +14 -19
  8. package/dist/AptifyConnector.d.ts +0 -95
  9. package/dist/AptifyConnector.d.ts.map +0 -1
  10. package/dist/AptifyConnector.js +0 -760
  11. package/dist/AptifyConnector.js.map +0 -1
  12. package/dist/BlackbaudConnector.d.ts +0 -64
  13. package/dist/BlackbaudConnector.d.ts.map +0 -1
  14. package/dist/BlackbaudConnector.js +0 -916
  15. package/dist/BlackbaudConnector.js.map +0 -1
  16. package/dist/ConstantContactConnector.d.ts +0 -137
  17. package/dist/ConstantContactConnector.d.ts.map +0 -1
  18. package/dist/ConstantContactConnector.js +0 -663
  19. package/dist/ConstantContactConnector.js.map +0 -1
  20. package/dist/CventConnector.d.ts +0 -146
  21. package/dist/CventConnector.d.ts.map +0 -1
  22. package/dist/CventConnector.js +0 -540
  23. package/dist/CventConnector.js.map +0 -1
  24. package/dist/DynamicsDataverseConnector.d.ts +0 -296
  25. package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
  26. package/dist/DynamicsDataverseConnector.js +0 -790
  27. package/dist/DynamicsDataverseConnector.js.map +0 -1
  28. package/dist/FileFeedConnector.d.ts +0 -64
  29. package/dist/FileFeedConnector.d.ts.map +0 -1
  30. package/dist/FileFeedConnector.js +0 -190
  31. package/dist/FileFeedConnector.js.map +0 -1
  32. package/dist/FontevaConnector.d.ts +0 -153
  33. package/dist/FontevaConnector.d.ts.map +0 -1
  34. package/dist/FontevaConnector.js +0 -421
  35. package/dist/FontevaConnector.js.map +0 -1
  36. package/dist/GrowthZoneConnector.d.ts +0 -183
  37. package/dist/GrowthZoneConnector.d.ts.map +0 -1
  38. package/dist/GrowthZoneConnector.js +0 -702
  39. package/dist/GrowthZoneConnector.js.map +0 -1
  40. package/dist/HivebriteConnector.d.ts +0 -151
  41. package/dist/HivebriteConnector.d.ts.map +0 -1
  42. package/dist/HivebriteConnector.js +0 -533
  43. package/dist/HivebriteConnector.js.map +0 -1
  44. package/dist/HubSpotConnector.d.ts +0 -523
  45. package/dist/HubSpotConnector.d.ts.map +0 -1
  46. package/dist/HubSpotConnector.js +0 -3110
  47. package/dist/HubSpotConnector.js.map +0 -1
  48. package/dist/IMISConnector.d.ts +0 -171
  49. package/dist/IMISConnector.d.ts.map +0 -1
  50. package/dist/IMISConnector.js +0 -695
  51. package/dist/IMISConnector.js.map +0 -1
  52. package/dist/MJToMJConnector.d.ts +0 -279
  53. package/dist/MJToMJConnector.d.ts.map +0 -1
  54. package/dist/MJToMJConnector.js +0 -893
  55. package/dist/MJToMJConnector.js.map +0 -1
  56. package/dist/MagnetMailConnector.d.ts +0 -163
  57. package/dist/MagnetMailConnector.d.ts.map +0 -1
  58. package/dist/MagnetMailConnector.js +0 -823
  59. package/dist/MagnetMailConnector.js.map +0 -1
  60. package/dist/MailchimpConnector.d.ts +0 -197
  61. package/dist/MailchimpConnector.d.ts.map +0 -1
  62. package/dist/MailchimpConnector.js +0 -791
  63. package/dist/MailchimpConnector.js.map +0 -1
  64. package/dist/MemberSuiteConnector.d.ts +0 -185
  65. package/dist/MemberSuiteConnector.d.ts.map +0 -1
  66. package/dist/MemberSuiteConnector.js +0 -762
  67. package/dist/MemberSuiteConnector.js.map +0 -1
  68. package/dist/NeonCRMConnector.d.ts +0 -273
  69. package/dist/NeonCRMConnector.d.ts.map +0 -1
  70. package/dist/NeonCRMConnector.js +0 -1039
  71. package/dist/NeonCRMConnector.js.map +0 -1
  72. package/dist/NetForumConnector.d.ts +0 -170
  73. package/dist/NetForumConnector.d.ts.map +0 -1
  74. package/dist/NetForumConnector.js +0 -778
  75. package/dist/NetForumConnector.js.map +0 -1
  76. package/dist/NetSuiteConnector.d.ts +0 -136
  77. package/dist/NetSuiteConnector.d.ts.map +0 -1
  78. package/dist/NetSuiteConnector.js +0 -703
  79. package/dist/NetSuiteConnector.js.map +0 -1
  80. package/dist/NimbleAMSConnector.d.ts +0 -142
  81. package/dist/NimbleAMSConnector.d.ts.map +0 -1
  82. package/dist/NimbleAMSConnector.js +0 -703
  83. package/dist/NimbleAMSConnector.js.map +0 -1
  84. package/dist/NoviConnector.d.ts +0 -178
  85. package/dist/NoviConnector.d.ts.map +0 -1
  86. package/dist/NoviConnector.js +0 -566
  87. package/dist/NoviConnector.js.map +0 -1
  88. package/dist/ORCIDConnector.d.ts +0 -207
  89. package/dist/ORCIDConnector.d.ts.map +0 -1
  90. package/dist/ORCIDConnector.js +0 -708
  91. package/dist/ORCIDConnector.js.map +0 -1
  92. package/dist/OpenWaterConnector.d.ts +0 -173
  93. package/dist/OpenWaterConnector.d.ts.map +0 -1
  94. package/dist/OpenWaterConnector.js +0 -893
  95. package/dist/OpenWaterConnector.js.map +0 -1
  96. package/dist/PathLMSConnector.d.ts +0 -395
  97. package/dist/PathLMSConnector.d.ts.map +0 -1
  98. package/dist/PathLMSConnector.js +0 -1281
  99. package/dist/PathLMSConnector.js.map +0 -1
  100. package/dist/PheedLoopConnector.d.ts +0 -119
  101. package/dist/PheedLoopConnector.d.ts.map +0 -1
  102. package/dist/PheedLoopConnector.js +0 -429
  103. package/dist/PheedLoopConnector.js.map +0 -1
  104. package/dist/PropFuelConnector.d.ts +0 -153
  105. package/dist/PropFuelConnector.d.ts.map +0 -1
  106. package/dist/PropFuelConnector.js +0 -450
  107. package/dist/PropFuelConnector.js.map +0 -1
  108. package/dist/QuickBooksConnector.d.ts +0 -92
  109. package/dist/QuickBooksConnector.d.ts.map +0 -1
  110. package/dist/QuickBooksConnector.js +0 -713
  111. package/dist/QuickBooksConnector.js.map +0 -1
  112. package/dist/RasaConnector.d.ts +0 -152
  113. package/dist/RasaConnector.d.ts.map +0 -1
  114. package/dist/RasaConnector.js +0 -929
  115. package/dist/RasaConnector.js.map +0 -1
  116. package/dist/Reach360Connector.d.ts +0 -175
  117. package/dist/Reach360Connector.d.ts.map +0 -1
  118. package/dist/Reach360Connector.js +0 -796
  119. package/dist/Reach360Connector.js.map +0 -1
  120. package/dist/RelationalDBConnector.d.ts +0 -108
  121. package/dist/RelationalDBConnector.d.ts.map +0 -1
  122. package/dist/RelationalDBConnector.js +0 -228
  123. package/dist/RelationalDBConnector.js.map +0 -1
  124. package/dist/RhythmConnector.d.ts +0 -188
  125. package/dist/RhythmConnector.d.ts.map +0 -1
  126. package/dist/RhythmConnector.js +0 -784
  127. package/dist/RhythmConnector.js.map +0 -1
  128. package/dist/SageIntacctConnector.d.ts +0 -337
  129. package/dist/SageIntacctConnector.d.ts.map +0 -1
  130. package/dist/SageIntacctConnector.js +0 -2081
  131. package/dist/SageIntacctConnector.js.map +0 -1
  132. package/dist/SalesforceConnector.d.ts +0 -461
  133. package/dist/SalesforceConnector.d.ts.map +0 -1
  134. package/dist/SalesforceConnector.js +0 -1862
  135. package/dist/SalesforceConnector.js.map +0 -1
  136. package/dist/SharePointConnector.d.ts +0 -188
  137. package/dist/SharePointConnector.d.ts.map +0 -1
  138. package/dist/SharePointConnector.js +0 -716
  139. package/dist/SharePointConnector.js.map +0 -1
  140. package/dist/WicketConnector.d.ts +0 -252
  141. package/dist/WicketConnector.d.ts.map +0 -1
  142. package/dist/WicketConnector.js +0 -938
  143. package/dist/WicketConnector.js.map +0 -1
  144. package/dist/WildApricotConnector.d.ts +0 -201
  145. package/dist/WildApricotConnector.d.ts.map +0 -1
  146. package/dist/WildApricotConnector.js +0 -1129
  147. package/dist/WildApricotConnector.js.map +0 -1
  148. package/dist/YourMembershipConnector.d.ts +0 -349
  149. package/dist/YourMembershipConnector.d.ts.map +0 -1
  150. package/dist/YourMembershipConnector.js +0 -4062
  151. package/dist/YourMembershipConnector.js.map +0 -1
  152. package/dist/generate-integration-actions.d.ts +0 -23
  153. package/dist/generate-integration-actions.d.ts.map +0 -1
  154. package/dist/generate-integration-actions.js +0 -201
  155. package/dist/generate-integration-actions.js.map +0 -1
@@ -1,716 +0,0 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { RegisterClass } from '@memberjunction/global';
8
- import { Metadata } from '@memberjunction/core';
9
- import { 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
- /**
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';
49
- // ─── Connector Implementation ────────────────────────────────────────
50
- /**
51
- * Connector for Microsoft SharePoint Online via Microsoft Graph v1.0.
52
- *
53
- * Extends BaseRESTIntegrationConnector — inherits pagination + template variable
54
- * substitution from metadata. Template vars like `{id}` / `{siteId}` are resolved
55
- * per-parent using IntegrationObject FK / PK metadata, so object hierarchies
56
- * (sites -> lists -> list items) traverse automatically.
57
- *
58
- * Auth flow:
59
- * 1. Client-credentials OAuth 2.0 against Azure AD
60
- * 2. Bearer token attached to every Graph request
61
- * 3. Token is refreshed proactively before expiry
62
- *
63
- * Pagination: `@odata.nextLink` cursor (per OData convention) — replayed verbatim.
64
- *
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.
72
- */
73
- let SharePointConnector = class SharePointConnector extends BaseRESTIntegrationConnector {
74
- constructor() {
75
- // ── State ────────────────────────────────────────────────────────
76
- super(...arguments);
77
- this.cachedAuth = null;
78
- this.tokenExpiresAt = 0;
79
- this.lastRequestTime = 0;
80
- }
81
- // ── Capability Getters ───────────────────────────────────────────
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
- */
104
- get IntegrationName() { return 'SharePoint'; }
105
- // ── Action Generation ────────────────────────────────────────────
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.
115
- // ── Default Configuration ────────────────────────────────────────
116
- GetDefaultConfiguration() {
117
- return {
118
- DefaultSchemaName: 'SharePoint',
119
- DefaultObjects: [
120
- {
121
- SourceObjectName: 'Site',
122
- TargetTableName: 'SharePoint_Site',
123
- TargetEntityName: 'SharePoint Sites',
124
- SyncEnabled: true,
125
- FieldMappings: this.GetDefaultFieldMappings('Site', 'Sites'),
126
- },
127
- {
128
- SourceObjectName: 'List',
129
- TargetTableName: 'SharePoint_List',
130
- TargetEntityName: 'SharePoint Lists',
131
- SyncEnabled: true,
132
- FieldMappings: this.GetDefaultFieldMappings('List', 'Lists'),
133
- },
134
- {
135
- SourceObjectName: 'ListItem',
136
- TargetTableName: 'SharePoint_ListItem',
137
- TargetEntityName: 'SharePoint List Items',
138
- SyncEnabled: true,
139
- FieldMappings: this.GetDefaultFieldMappings('ListItem', 'ListItems'),
140
- },
141
- ],
142
- };
143
- }
144
- GetDefaultFieldMappings(objectName, _entityName) {
145
- switch (objectName) {
146
- case 'Site':
147
- return [
148
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
149
- { SourceFieldName: 'displayName', DestinationFieldName: 'Name' },
150
- { SourceFieldName: 'webUrl', DestinationFieldName: 'URL' },
151
- { SourceFieldName: 'description', DestinationFieldName: 'Description' },
152
- ];
153
- case 'List':
154
- return [
155
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
156
- { SourceFieldName: 'displayName', DestinationFieldName: 'Name' },
157
- { SourceFieldName: 'description', DestinationFieldName: 'Description' },
158
- ];
159
- case 'ListItem':
160
- return [
161
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
162
- ];
163
- default:
164
- return [];
165
- }
166
- }
167
- // ── TestConnection ───────────────────────────────────────────────
168
- /**
169
- * Verifies the service principal can reach Graph by fetching the tenant's
170
- * organization record. Forces a token refresh so credentials are validated.
171
- */
172
- async TestConnection(companyIntegration, contextUser) {
173
- try {
174
- const auth = await this.Authenticate(companyIntegration, contextUser, true);
175
- const url = `${auth.BaseUrl}/organization?$select=id,displayName`;
176
- const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
177
- this.ValidateResponse(response, url);
178
- const body = response.Body;
179
- const orgName = body.value?.[0]?.displayName ?? 'unknown';
180
- return {
181
- Success: true,
182
- Message: `Successfully connected to Microsoft Graph (tenant: ${orgName})`,
183
- ServerVersion: 'Microsoft Graph v1.0',
184
- };
185
- }
186
- catch (err) {
187
- const message = err instanceof Error ? err.message : String(err);
188
- return { Success: false, Message: `Connection failed: ${message}` };
189
- }
190
- }
191
- // ── DiscoverObjects / DiscoverFields ─────────────────────────────
192
- /**
193
- * Returns the static IntegrationObject list. Discovery of per-site custom
194
- * lists happens via the SiteLists IntegrationObject's FetchChanges loop, not
195
- * here — this is a capability catalog, not a live enumeration.
196
- */
197
- async DiscoverObjects(companyIntegration, contextUser) {
198
- return super.DiscoverObjects(companyIntegration, contextUser);
199
- }
200
- /**
201
- * Returns static field metadata from IntegrationEngineBase, plus — for
202
- * SiteListItems — augments with live list-column discovery per list.
203
- * Custom columns are marked via the IsReadOnly=false / isSealed=false
204
- * Graph-reported flags; callers downstream can set IsCustom accordingly.
205
- */
206
- async DiscoverFields(companyIntegration, objectName, contextUser) {
207
- const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
208
- if (objectName !== LIST_ITEM_OBJECT)
209
- return staticFields;
210
- const custom = await this.DiscoverListColumnsForAllLists(companyIntegration, contextUser, staticFields);
211
- return [...staticFields, ...custom];
212
- }
213
- // ── FetchChanges (Template Variable / Delta hooks) ───────────────
214
- /**
215
- * Top-level FetchChanges delegates to BaseRESTIntegrationConnector for
216
- * template-variable resolution and pagination. We intercept the top-level
217
- * Sites endpoint because the /sites?search=* syntax is not a standard GET
218
- * collection — we normalise its response to the value array ourselves.
219
- */
220
- async FetchChanges(ctx) {
221
- const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
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)) {
229
- return this.FetchChangesViaDelta(ctx, obj);
230
- }
231
- return super.FetchChanges(ctx);
232
- }
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
- */
258
- async CreateRecord(ctx) {
259
- if (ctx.ObjectName !== LISTITEM_CREATE_OBJECT) {
260
- return super.CreateRecord(ctx);
261
- }
262
- const ci = ctx.CompanyIntegration;
263
- const contextUser = ctx.ContextUser;
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.`);
268
- }
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);
283
- }
284
- return {
285
- Success: false,
286
- StatusCode: response.Status,
287
- ErrorMessage: this.ExtractErrorMessage(response) ?? `HTTP ${response.Status} on create`,
288
- };
289
- }
290
- // ─── Abstract BaseRESTIntegrationConnector hooks ────────────────
291
- /**
292
- * Authenticates with Azure AD via client-credentials flow.
293
- * Caches the bearer token until within TOKEN_REFRESH_BUFFER_MS of expiry.
294
- */
295
- async Authenticate(companyIntegration, contextUser, forceRefresh = false) {
296
- if (!forceRefresh && this.cachedAuth && this.IsTokenValid()) {
297
- return this.cachedAuth;
298
- }
299
- const config = await this.ParseConfig(companyIntegration, contextUser);
300
- const token = await this.RequestGraphToken(config);
301
- const baseUrl = config.GraphBaseUrl ?? GRAPH_V1_BASE_URL;
302
- const auth = {
303
- Token: token.access_token,
304
- ExpiresAt: new Date(Date.now() + token.expires_in * 1000),
305
- Config: config,
306
- BaseUrl: baseUrl,
307
- };
308
- this.cachedAuth = auth;
309
- this.tokenExpiresAt = Date.now() + token.expires_in * 1000;
310
- return auth;
311
- }
312
- BuildHeaders(auth) {
313
- const token = auth.Token ?? '';
314
- return {
315
- 'Authorization': `Bearer ${token}`,
316
- 'Accept': 'application/json',
317
- 'Content-Type': 'application/json',
318
- };
319
- }
320
- /**
321
- * HTTP transport with rate-limit throttling, Retry-After honoring, and
322
- * exponential backoff on 429/503.
323
- */
324
- async MakeHTTPRequest(auth, url, method, headers, body) {
325
- const spAuth = auth;
326
- const config = spAuth.Config;
327
- const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
328
- const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
329
- const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
330
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
331
- await this.Throttle(minInterval);
332
- const response = await this.ExecuteOneRequest(url, method, headers, body, timeoutMs);
333
- if (this.IsRetryable(response) && attempt < maxRetries) {
334
- const delay = this.ComputeBackoffDelay(attempt, response.Headers['retry-after']);
335
- await this.Sleep(delay);
336
- continue;
337
- }
338
- return response;
339
- }
340
- throw new Error(`SharePointConnector: exhausted ${maxRetries + 1} attempts for ${method} ${url}`);
341
- }
342
- /**
343
- * Extracts the `value` array from a Graph response envelope. The base class
344
- * already passes `ResponseDataKey` from metadata — we default to the `value`
345
- * convention when responseDataKey is null.
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
- }
364
- NormalizeResponse(rawBody, responseDataKey) {
365
- const body = rawBody;
366
- const key = responseDataKey ?? 'value';
367
- const data = body[key];
368
- if (Array.isArray(data))
369
- return this.ExcludePersonalSites(data);
370
- // Single-record response fallback
371
- if (body && typeof body === 'object' && 'id' in body)
372
- return this.ExcludePersonalSites([body]);
373
- return [];
374
- }
375
- /**
376
- * Extracts pagination state from the Graph `@odata.nextLink` annotation.
377
- * Graph uses opaque URL cursors — we store the full nextLink URL in the
378
- * cursor so the next iteration issues the exact request Graph expects.
379
- */
380
- ExtractPaginationInfo(rawBody, _paginationType, _currentPage, _currentOffset, _pageSize) {
381
- const body = rawBody;
382
- const nextLink = body?.['@odata.nextLink'];
383
- return {
384
- HasMore: typeof nextLink === 'string' && nextLink.length > 0,
385
- NextCursor: nextLink,
386
- };
387
- }
388
- GetBaseURL(_companyIntegration, auth) {
389
- const spAuth = auth;
390
- return spAuth.BaseUrl;
391
- }
392
- /**
393
- * Overrides the base pagination URL builder. Graph's nextLink is an absolute
394
- * URL that encodes the full continuation request. When we have a cursor we
395
- * use it directly; otherwise we rely on the base path (already wired with
396
- * $top / $select via DefaultQueryParams).
397
- */
398
- BuildPaginatedURL(basePath, obj, _page, _offset, cursor, effectivePageSize) {
399
- if (cursor && cursor.length > 0)
400
- return cursor;
401
- const pageSize = effectivePageSize ?? obj.DefaultPageSize ?? 200;
402
- const separator = basePath.includes('?') ? '&' : '?';
403
- return `${basePath}${separator}$top=${pageSize}`;
404
- }
405
- // ─── Token Management ────────────────────────────────────────────
406
- IsTokenValid() {
407
- if (!this.cachedAuth || !this.cachedAuth.Token)
408
- return false;
409
- return Date.now() < this.tokenExpiresAt - TOKEN_REFRESH_BUFFER_MS;
410
- }
411
- async RequestGraphToken(config) {
412
- const url = AAD_TOKEN_ENDPOINT(config.TenantId);
413
- const scope = config.Scope ?? DEFAULT_SCOPE;
414
- const form = new URLSearchParams({
415
- client_id: config.ClientId,
416
- scope,
417
- client_secret: config.ClientSecret,
418
- grant_type: 'client_credentials',
419
- });
420
- const response = await fetch(url, {
421
- method: 'POST',
422
- headers: {
423
- 'Content-Type': 'application/x-www-form-urlencoded',
424
- 'Accept': 'application/json',
425
- },
426
- body: form.toString(),
427
- });
428
- const text = await response.text();
429
- if (!response.ok) {
430
- throw new Error(`SharePointConnector: Azure AD token request failed (${response.status}): ${text.slice(0, 500)}`);
431
- }
432
- return JSON.parse(text);
433
- }
434
- // ─── Configuration Parsing ───────────────────────────────────────
435
- async ParseConfig(companyIntegration, contextUser) {
436
- const credentialID = companyIntegration.CredentialID;
437
- if (credentialID) {
438
- const config = await this.LoadFromCredentialEntity(credentialID, contextUser);
439
- if (config)
440
- return config;
441
- }
442
- const configJson = companyIntegration.Configuration;
443
- if (configJson) {
444
- const parsed = JSON.parse(configJson);
445
- return this.ValidateConfig(parsed);
446
- }
447
- throw new Error('SharePointConnector: No credentials or configuration found on CompanyIntegration');
448
- }
449
- async LoadFromCredentialEntity(credentialID, contextUser, provider) {
450
- const md = provider ?? new Metadata();
451
- const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
452
- const loaded = await credential.Load(credentialID);
453
- if (!loaded || !credential.Values)
454
- return null;
455
- try {
456
- const raw = JSON.parse(credential.Values);
457
- const parsed = {
458
- TenantId: raw.tenantId ?? raw.TenantId,
459
- ClientId: raw.clientId ?? raw.ClientId,
460
- ClientSecret: raw.clientSecret ?? raw.ClientSecret,
461
- GraphBaseUrl: raw.graphBaseUrl ?? raw.GraphBaseUrl,
462
- Scope: raw.scope ?? raw.Scope,
463
- };
464
- return this.ValidateConfig(parsed);
465
- }
466
- catch {
467
- return null;
468
- }
469
- }
470
- ValidateConfig(raw) {
471
- if (!raw.TenantId)
472
- throw new Error('SharePointConnector: TenantId is required');
473
- if (!raw.ClientId)
474
- throw new Error('SharePointConnector: ClientId is required');
475
- if (!raw.ClientSecret)
476
- throw new Error('SharePointConnector: ClientSecret is required');
477
- return {
478
- TenantId: raw.TenantId,
479
- ClientId: raw.ClientId,
480
- ClientSecret: raw.ClientSecret,
481
- GraphBaseUrl: raw.GraphBaseUrl,
482
- Scope: raw.Scope,
483
- MaxRetries: raw.MaxRetries ?? DEFAULT_MAX_RETRIES,
484
- RequestTimeoutMs: raw.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
485
- MinRequestIntervalMs: raw.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
486
- };
487
- }
488
- // ─── HTTP Helpers ────────────────────────────────────────────────
489
- async ExecuteOneRequest(url, method, headers, body, timeoutMs) {
490
- const controller = new AbortController();
491
- const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
492
- try {
493
- const response = await fetch(url, {
494
- method,
495
- headers,
496
- body: body !== undefined && method !== 'GET' && method !== 'DELETE'
497
- ? JSON.stringify(body)
498
- : undefined,
499
- signal: controller.signal,
500
- });
501
- this.lastRequestTime = Date.now();
502
- const responseHeaders = this.ExtractHeaders(response.headers);
503
- const parsedBody = await this.ParseResponseBody(response);
504
- return { Status: response.status, Body: parsedBody, Headers: responseHeaders };
505
- }
506
- finally {
507
- clearTimeout(timeoutHandle);
508
- }
509
- }
510
- async ParseResponseBody(response) {
511
- const text = await response.text();
512
- if (!text)
513
- return null;
514
- try {
515
- return JSON.parse(text);
516
- }
517
- catch {
518
- return text;
519
- }
520
- }
521
- ExtractHeaders(headers) {
522
- const map = {};
523
- headers.forEach((value, key) => {
524
- map[key.toLowerCase()] = value;
525
- });
526
- return map;
527
- }
528
- IsRetryable(response) {
529
- return response.Status === 429 || response.Status === 503 || response.Status === 504;
530
- }
531
- ComputeBackoffDelay(attempt, retryAfterHeader) {
532
- if (retryAfterHeader) {
533
- const parsed = parseInt(retryAfterHeader, 10);
534
- if (!Number.isNaN(parsed) && parsed > 0) {
535
- return Math.min(parsed * 1000, 60000);
536
- }
537
- }
538
- return Math.min(Math.pow(2, attempt) * 1000, 30000);
539
- }
540
- async Throttle(minIntervalMs) {
541
- const elapsed = Date.now() - this.lastRequestTime;
542
- if (elapsed < minIntervalMs) {
543
- await this.Sleep(minIntervalMs - elapsed);
544
- }
545
- }
546
- Sleep(ms) {
547
- return new Promise(resolve => setTimeout(resolve, ms));
548
- }
549
- ValidateResponse(response, url) {
550
- if (response.Status < 200 || response.Status >= 300) {
551
- const preview = typeof response.Body === 'string'
552
- ? response.Body.slice(0, 500)
553
- : JSON.stringify(response.Body).slice(0, 500);
554
- throw new Error(`Graph HTTP ${response.Status} from ${url}: ${preview}`);
555
- }
556
- }
557
- // ─── Delta sync ──────────────────────────────────────────────────
558
- /**
559
- * Detects whether an IntegrationObject supports Graph `/delta` queries.
560
- * Per the frozen contract, exactly Site, DriveItem, and ListItem carry
561
- * `IncrementalWatermarkField=@odata.deltaLink`.
562
- */
563
- SupportsDeltaForObject(obj) {
564
- return DELTA_OBJECTS.has(obj.Name);
565
- }
566
- /**
567
- * Fetches changed records via Graph's /delta endpoint. The `WatermarkValue`
568
- * holds either a full delta URL (from the previous batch) or a token to
569
- * append to the delta endpoint.
570
- */
571
- async FetchChangesViaDelta(ctx, obj) {
572
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
573
- const deltaUrl = this.BuildDeltaUrl(auth, obj, ctx.WatermarkValue);
574
- const response = await this.MakeHTTPRequest(auth, deltaUrl, 'GET', this.BuildHeaders(auth));
575
- this.ValidateResponse(response, deltaUrl);
576
- const body = response.Body;
577
- const records = body.value ?? [];
578
- const nextLink = body['@odata.nextLink'];
579
- const deltaLink = body['@odata.deltaLink'];
580
- const externalRecords = records.map(r => ({
581
- ExternalID: String(r['id'] ?? ''),
582
- ObjectType: ctx.ObjectName,
583
- Fields: r,
584
- ModifiedAt: typeof r['lastModifiedDateTime'] === 'string'
585
- ? new Date(r['lastModifiedDateTime']) : undefined,
586
- // Microsoft Graph delta deletion semantics: driveItem and listItem use the
587
- // `deleted` facet (e.g. `"deleted": { "state": "deleted" }`). The legacy
588
- // `@removed` shape applies to directoryObjects (users/groups) only. Check
589
- // both so we catch deletions across the resource types this connector reaches.
590
- IsDeleted: (typeof r['deleted'] === 'object' && r['deleted'] !== null) ||
591
- (typeof r['@removed'] === 'object' && r['@removed'] !== null),
592
- }));
593
- return {
594
- Records: externalRecords,
595
- HasMore: Boolean(nextLink),
596
- NewWatermarkValue: deltaLink ?? nextLink,
597
- NextCursor: nextLink,
598
- };
599
- }
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))
607
- return watermark;
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.`);
612
- }
613
- // ─── Runtime List-Column Discovery (Custom Fields) ───────────────
614
- /**
615
- * Enumerates every accessible (site, list) pair and fetches each list's
616
- * column definitions, returning a deduplicated array of fields not already
617
- * present in the static metadata.
618
- *
619
- * Note: For large tenants this can be expensive. Callers typically scope
620
- * this to specific sites via ExtraFilter / configuration.
621
- */
622
- async DiscoverListColumnsForAllLists(companyIntegration, contextUser, staticFields) {
623
- try {
624
- const auth = await this.Authenticate(companyIntegration, contextUser);
625
- const sites = await this.ListAllSites(auth);
626
- const staticFieldNames = new Set(staticFields.map(f => f.Name.toLowerCase()));
627
- const seen = new Set();
628
- const results = [];
629
- for (const site of sites) {
630
- const lists = await this.ListListsForSite(auth, site.id);
631
- for (const list of lists) {
632
- const columns = await this.ListColumnsForList(auth, site.id, list.id);
633
- for (const col of columns) {
634
- const name = col.name ?? col.id;
635
- if (!name)
636
- continue;
637
- const key = name.toLowerCase();
638
- if (staticFieldNames.has(key) || seen.has(key))
639
- continue;
640
- seen.add(key);
641
- results.push(this.GraphColumnToFieldSchema(col));
642
- }
643
- }
644
- }
645
- return results;
646
- }
647
- catch {
648
- // Best-effort — degrade gracefully if live discovery fails
649
- return [];
650
- }
651
- }
652
- async ListAllSites(auth) {
653
- const url = `${auth.BaseUrl}/sites?search=*&$select=id,displayName,name,webUrl`;
654
- const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
655
- this.ValidateResponse(response, url);
656
- const body = response.Body;
657
- return body.value ?? [];
658
- }
659
- async ListListsForSite(auth, siteId) {
660
- const url = `${auth.BaseUrl}/sites/${encodeURIComponent(siteId)}/lists?$select=id,name,displayName`;
661
- const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
662
- if (response.Status >= 400)
663
- return [];
664
- const body = response.Body;
665
- return body.value ?? [];
666
- }
667
- async ListColumnsForList(auth, siteId, listId) {
668
- const url = `${auth.BaseUrl}/sites/${encodeURIComponent(siteId)}/lists/${encodeURIComponent(listId)}/columns`;
669
- const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
670
- if (response.Status >= 400)
671
- return [];
672
- const body = response.Body;
673
- return body.value ?? [];
674
- }
675
- GraphColumnToFieldSchema(col) {
676
- return {
677
- Name: col.name ?? col.id ?? 'unknown',
678
- Label: col.displayName ?? col.name ?? col.id ?? 'unknown',
679
- Description: col.description ?? undefined,
680
- DataType: this.InferGraphColumnType(col),
681
- IsRequired: col.required === true,
682
- IsUniqueKey: false,
683
- IsReadOnly: col.readOnly === true,
684
- IsForeignKey: false,
685
- ForeignKeyTarget: null,
686
- };
687
- }
688
- InferGraphColumnType(col) {
689
- if (col.text)
690
- return 'string';
691
- if (col.number)
692
- return 'decimal';
693
- if (col.boolean)
694
- return 'boolean';
695
- if (col.dateTime)
696
- return 'datetime';
697
- if (col.choice)
698
- return 'string';
699
- if (col.currency)
700
- return 'decimal';
701
- if (col.lookup)
702
- return 'string';
703
- if (col.personOrGroup)
704
- return 'string';
705
- if (col.hyperlinkOrPicture)
706
- return 'string';
707
- return 'string';
708
- }
709
- };
710
- SharePointConnector = __decorate([
711
- RegisterClass(BaseIntegrationConnector, 'SharePointConnector')
712
- ], SharePointConnector);
713
- export { SharePointConnector };
714
- /** Tree-shaking prevention function — import and call from module entry point. */
715
- export function LoadSharePointConnector() { }
716
- //# sourceMappingURL=SharePointConnector.js.map