@memberjunction/integration-connectors 5.51.0 → 6.1.0-edge.1

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,916 +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
- /**
8
- * BlackbaudConnector — Integration connector for Blackbaud Raiser's Edge NXT (SKY API).
9
- *
10
- * API Documentation: https://developer.blackbaud.com/skyapi/
11
- *
12
- * Auth: OAuth 2.0 (access_token + refresh_token) PLUS subscription key header.
13
- * - Authorization: Bearer {access_token}
14
- * - bb-api-subscription-key: {subscription_key}
15
- * Refresh via https://oauth2.sky.blackbaud.com/token
16
- * Base URL: https://api.sky.blackbaud.com/
17
- * Pagination: Offset-based via next_link in response
18
- * Rate limits: 10 calls/second hard limit — enforced with 120ms minimum interval
19
- * Incremental: Date filter params on list endpoints
20
- * CRUD: Full on Constituents, Gifts, Pledges, Notes, Relationships
21
- *
22
- * API Categories:
23
- * - Constituent API (implemented) — constituents, addresses, phones, emails, relationships, notes
24
- * - Gift API (implemented) — gifts, pledges, gift splits, soft credits
25
- * - Fundraising API (implemented) — fundraisers, campaigns, appeals, funds
26
- * - Opportunity API (implemented, read-only) — prospect opportunities
27
- * - Event API (NOT implemented) — separate Sky Signup product, not RE NXT
28
- * - Education API (NOT implemented) — Higher Education product, separate license
29
- * - Merchant Services API (NOT implemented) — payment terminal APIs, hardware-specific
30
- * - Church Management (NOT implemented) — separate product (Church Management)
31
- * - Webhooks (available) — Constituent, Gift, Payment, Custom Field events; not implemented as receiver
32
- */
33
- import { RegisterClass } from '@memberjunction/global';
34
- import { Metadata } from '@memberjunction/core';
35
- import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
36
- // ─── Constants ────────────────────────────────────────────────────────────
37
- const BB_TOKEN_URL = 'https://oauth2.sky.blackbaud.com/token';
38
- const BB_API_BASE = 'https://api.sky.blackbaud.com';
39
- const BB_PAGE_SIZE = 500;
40
- const TOKEN_REFRESH_BUFFER_MS = 60_000;
41
- const MAX_RETRIES = 3;
42
- const REQUEST_TIMEOUT_MS = 30_000;
43
- // 10 calls/second = 100ms minimum. Use 120ms to stay comfortably under.
44
- const MIN_REQUEST_INTERVAL_MS = 120;
45
- // ─── Static Object Definitions ────────────────────────────────────────────
46
- const BB_OBJECTS = [
47
- {
48
- Name: 'Constituents',
49
- DisplayName: 'Constituent',
50
- Description: 'Individual donors, prospects, volunteers, and organizational constituents',
51
- SupportsWrite: true,
52
- Fields: [
53
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Constituent GUID' },
54
- { Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Individual or Organization' },
55
- { Name: 'lookup_id', DisplayName: 'Lookup ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'External lookup ID' },
56
- { Name: 'first', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'First name (individuals)' },
57
- { Name: 'last', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Last name (individuals)' },
58
- { Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Full name or organization name' },
59
- { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email address object' },
60
- { Name: 'phone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary phone object' },
61
- { Name: 'address', DisplayName: 'Address', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary address object' },
62
- { Name: 'gender', DisplayName: 'Gender', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Gender (individuals)' },
63
- { Name: 'birthdate', DisplayName: 'Birthdate', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Date of birth (individuals)' },
64
- { Name: 'deceased', DisplayName: 'Deceased', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether constituent is deceased' },
65
- { Name: 'inactive', DisplayName: 'Inactive', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether constituent is inactive' },
66
- { Name: 'date_added', DisplayName: 'Date Added', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Record creation date' },
67
- { Name: 'date_modified', DisplayName: 'Date Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modification date' },
68
- ],
69
- },
70
- {
71
- Name: 'ConstituentAddresses',
72
- DisplayName: 'Constituent Address',
73
- Description: 'Addresses for constituents (parent-child under Constituents)',
74
- SupportsWrite: true,
75
- Fields: [
76
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Address GUID' },
77
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Parent constituent GUID' },
78
- { Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Address type (Home, Business, etc.)' },
79
- { Name: 'address_lines', DisplayName: 'Street', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Street address lines' },
80
- { Name: 'city', DisplayName: 'City', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'City' },
81
- { Name: 'state', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'State/province' },
82
- { Name: 'postal_code', DisplayName: 'Postal Code', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'ZIP/postal code' },
83
- { Name: 'country', DisplayName: 'Country', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Country' },
84
- { Name: 'preferred', DisplayName: 'Preferred', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether this is the preferred address' },
85
- ],
86
- },
87
- {
88
- Name: 'ConstituentPhones',
89
- DisplayName: 'Constituent Phone',
90
- Description: 'Phone numbers for constituents (parent-child under Constituents)',
91
- SupportsWrite: true,
92
- Fields: [
93
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Phone GUID' },
94
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Parent constituent GUID' },
95
- { Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Phone type (Home, Business, Mobile, etc.)' },
96
- { Name: 'number', DisplayName: 'Number', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Phone number' },
97
- { Name: 'primary', DisplayName: 'Primary', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether this is the primary phone' },
98
- ],
99
- },
100
- {
101
- Name: 'ConstituentEmails',
102
- DisplayName: 'Constituent Email',
103
- Description: 'Email addresses for constituents (parent-child under Constituents)',
104
- SupportsWrite: true,
105
- Fields: [
106
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Email GUID' },
107
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Parent constituent GUID' },
108
- { Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Email type (Personal, Business, etc.)' },
109
- { Name: 'address', DisplayName: 'Address', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Email address value' },
110
- { Name: 'primary', DisplayName: 'Primary', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether this is the primary email' },
111
- ],
112
- },
113
- {
114
- Name: 'Gifts',
115
- DisplayName: 'Gift',
116
- Description: 'Donation and gift records linked to constituents',
117
- SupportsWrite: true,
118
- Fields: [
119
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Gift GUID' },
120
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Donor constituent GUID' },
121
- { Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Gift type (Donation, RecurringGift, GiftInKind, etc.)' },
122
- { Name: 'amount', DisplayName: 'Amount', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Gift amount object { value, currency }' },
123
- { Name: 'date', DisplayName: 'Gift Date', Type: 'datetime', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Gift date' },
124
- { Name: 'fund', DisplayName: 'Fund', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Fund designation' },
125
- { Name: 'campaign', DisplayName: 'Campaign', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Campaign designation' },
126
- { Name: 'appeal', DisplayName: 'Appeal', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Appeal designation' },
127
- { Name: 'acknowledgement', DisplayName: 'Acknowledgement', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Acknowledgement status' },
128
- { Name: 'date_added', DisplayName: 'Date Added', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Record creation date' },
129
- { Name: 'date_modified', DisplayName: 'Date Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modification date' },
130
- ],
131
- },
132
- {
133
- Name: 'Fundraisers',
134
- DisplayName: 'Fundraiser',
135
- Description: 'Fundraising campaigns and appeals',
136
- SupportsWrite: false,
137
- Fields: [
138
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Fundraiser GUID' },
139
- { Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Fundraiser name/description' },
140
- { Name: 'start_date', DisplayName: 'Start Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Fundraiser start date' },
141
- { Name: 'end_date', DisplayName: 'End Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Fundraiser end date' },
142
- { Name: 'goal', DisplayName: 'Goal', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Fundraising goal amount' },
143
- ],
144
- },
145
- {
146
- Name: 'Campaigns',
147
- DisplayName: 'Campaign',
148
- Description: 'Fundraising campaigns',
149
- SupportsWrite: false,
150
- Fields: [
151
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Campaign GUID' },
152
- { Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign name/description' },
153
- { Name: 'start_date', DisplayName: 'Start Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign start date' },
154
- { Name: 'end_date', DisplayName: 'End Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign end date' },
155
- { Name: 'goal', DisplayName: 'Goal', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign goal' },
156
- ],
157
- },
158
- {
159
- Name: 'Opportunities',
160
- DisplayName: 'Opportunity',
161
- Description: 'Major gift prospect opportunities (read-only)',
162
- SupportsWrite: false,
163
- Fields: [
164
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Opportunity GUID' },
165
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Prospect constituent GUID' },
166
- { Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Opportunity name' },
167
- { Name: 'status', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Opportunity status' },
168
- { Name: 'ask_date', DisplayName: 'Ask Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Date of ask' },
169
- { Name: 'ask_amount', DisplayName: 'Ask Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Amount asked' },
170
- { Name: 'expected_date', DisplayName: 'Expected Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Expected close date' },
171
- ],
172
- },
173
- // ─── Additional SKY API endpoints — lean overlay ──
174
- // Constituent sub-resources
175
- { Name: 'ConstituentRelationships', DisplayName: 'Relationship', Description: 'Constituent relationships', SupportsWrite: true, Fields: [
176
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Relationship ID' },
177
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
178
- ] },
179
- { Name: 'ConstituentNotes', DisplayName: 'Note', Description: 'Constituent notes', SupportsWrite: true, Fields: [
180
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Note ID' },
181
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
182
- { Name: 'date_added', DisplayName: 'Added', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
183
- ] },
184
- { Name: 'ConstituentActions', DisplayName: 'Action', Description: 'Constituent actions/activities', SupportsWrite: true, Fields: [
185
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Action ID' },
186
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
187
- { Name: 'date', DisplayName: 'Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Incremental date' },
188
- ] },
189
- { Name: 'ConstituentCustomFields', DisplayName: 'Custom Field', Description: 'Constituent custom field values', SupportsWrite: true, Fields: [
190
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom field ID' },
191
- { Name: 'parent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
192
- ] },
193
- { Name: 'ConstituentCodes', DisplayName: 'Constituent Code', Description: 'Constituent classification codes', SupportsWrite: true, Fields: [
194
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Code ID' },
195
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
196
- ] },
197
- { Name: 'ConstituentEducation', DisplayName: 'Education', Description: 'Constituent education records', SupportsWrite: true, Fields: [
198
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Education ID' },
199
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
200
- ] },
201
- { Name: 'ConstituentOnlinePresence', DisplayName: 'Online Presence', Description: 'Social media/web profiles', SupportsWrite: true, Fields: [
202
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Presence ID' },
203
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
204
- ] },
205
- { Name: 'ConstituentMemberships', DisplayName: 'Membership', Description: 'Constituent memberships', SupportsWrite: true, Fields: [
206
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Membership ID' },
207
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
208
- ] },
209
- // Gift sub-resources
210
- { Name: 'GiftCustomFields', DisplayName: 'Gift Custom Field', Description: 'Custom field values on gifts', SupportsWrite: true, Fields: [
211
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom field ID' },
212
- { Name: 'parent_id', DisplayName: 'Gift ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Gifts' },
213
- ] },
214
- { Name: 'GiftSoftCredits', DisplayName: 'Soft Credit', Description: 'Soft credit allocations on gifts', SupportsWrite: true, Fields: [
215
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Soft credit ID' },
216
- { Name: 'gift_id', DisplayName: 'Gift ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Gifts' },
217
- ] },
218
- // Pledges & Recurring Gifts
219
- { Name: 'Pledges', DisplayName: 'Pledge', Description: 'Pledge commitments', SupportsWrite: true, Fields: [
220
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Pledge ID' },
221
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
222
- { Name: 'date_added', DisplayName: 'Added', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
223
- ] },
224
- { Name: 'RecurringGifts', DisplayName: 'Recurring Gift', Description: 'Recurring gift/donation schedules', SupportsWrite: false, Fields: [
225
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Recurring gift ID' },
226
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
227
- ] },
228
- // Funds & Appeals
229
- { Name: 'Funds', DisplayName: 'Fund', Description: 'Fund designations', SupportsWrite: true, Fields: [
230
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Fund ID' },
231
- { Name: 'date_added', DisplayName: 'Added', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
232
- ] },
233
- { Name: 'Appeals', DisplayName: 'Appeal', Description: 'Fundraising appeals', SupportsWrite: true, Fields: [
234
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Appeal ID' },
235
- { Name: 'date_added', DisplayName: 'Added', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
236
- ] },
237
- // Events
238
- { Name: 'Events', DisplayName: 'Event', Description: 'Fundraising/special events', SupportsWrite: true, Fields: [
239
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Event ID' },
240
- { Name: 'start_date', DisplayName: 'Start', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Incremental date' },
241
- ] },
242
- { Name: 'EventParticipants', DisplayName: 'Event Participant', Description: 'Event attendees/registrants', SupportsWrite: true, Fields: [
243
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Participant ID' },
244
- { Name: 'event_id', DisplayName: 'Event ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Events' },
245
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
246
- ] },
247
- // Lists
248
- { Name: 'Lists', DisplayName: 'List', Description: 'Constituent lists', SupportsWrite: true, Fields: [
249
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'List ID' },
250
- { Name: 'date_modified', DisplayName: 'Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
251
- ] },
252
- // Tributes
253
- { Name: 'Tributes', DisplayName: 'Tribute', Description: 'Gift tribute/memorial records', SupportsWrite: true, Fields: [
254
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Tribute ID' },
255
- ] },
256
- // GL
257
- { Name: 'GLAccounts', DisplayName: 'GL Account', Description: 'General ledger accounts', SupportsWrite: true, Fields: [
258
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'GL Account ID' },
259
- ] },
260
- { Name: 'GLTransactionDistributions', DisplayName: 'GL Distribution', Description: 'GL transaction distributions', SupportsWrite: false, Fields: [
261
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Distribution ID' },
262
- { Name: 'post_date', DisplayName: 'Post Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
263
- ] },
264
- // Lookup types (read-only)
265
- { Name: 'MembershipTypes', DisplayName: 'Membership Type', Description: 'Membership type definitions', SupportsWrite: false, Fields: [
266
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
267
- ] },
268
- { Name: 'GiftTypes', DisplayName: 'Gift Type', Description: 'Gift type lookup', SupportsWrite: false, Fields: [
269
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
270
- ] },
271
- { Name: 'ActionTypes', DisplayName: 'Action Type', Description: 'Action type lookup', SupportsWrite: false, Fields: [
272
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
273
- ] },
274
- { Name: 'NoteTypes', DisplayName: 'Note Type', Description: 'Note type lookup', SupportsWrite: false, Fields: [
275
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
276
- ] },
277
- { Name: 'FundTypes', DisplayName: 'Fund Type', Description: 'Fund type lookup', SupportsWrite: false, Fields: [
278
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
279
- ] },
280
- // ─── Remaining Blackbaud sub-resources ──
281
- { Name: 'ConstituentAliases', DisplayName: 'Alias', Description: 'Constituent name aliases', SupportsWrite: true, Fields: [
282
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Alias ID' },
283
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
284
- ] },
285
- { Name: 'ConstituentSolicitCodes', DisplayName: 'Solicit Code', Description: 'Solicitation codes', SupportsWrite: true, Fields: [
286
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Code ID' },
287
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
288
- ] },
289
- { Name: 'ConstituentRatings', DisplayName: 'Rating', Description: 'Constituent prospect ratings', SupportsWrite: true, Fields: [
290
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Rating ID' },
291
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
292
- ] },
293
- { Name: 'ConstituentAttachments', DisplayName: 'Constituent Attachment', Description: 'File attachments on constituents', SupportsWrite: true, Fields: [
294
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Attachment ID' },
295
- { Name: 'parent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
296
- ] },
297
- { Name: 'ConstituentConsent', DisplayName: 'GDPR Consent', Description: 'GDPR consent records', SupportsWrite: true, Fields: [
298
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'FK → Constituents' },
299
- ] },
300
- { Name: 'ConstituentFundraiserAssignments', DisplayName: 'Fundraiser Assignment', Description: 'Fundraiser-to-constituent assignments', SupportsWrite: false, Fields: [
301
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Assignment ID' },
302
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Constituents' },
303
- ] },
304
- // Action sub-resources
305
- { Name: 'ConstituentActionCustomFields', DisplayName: 'Action Custom Field', Description: 'Custom fields on actions', SupportsWrite: true, Fields: [
306
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom field ID' },
307
- { Name: 'parent_id', DisplayName: 'Action ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → ConstituentActions' },
308
- ] },
309
- { Name: 'ConstituentActionAttachments', DisplayName: 'Action Attachment', Description: 'File attachments on actions', SupportsWrite: true, Fields: [
310
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Attachment ID' },
311
- { Name: 'parent_id', DisplayName: 'Action ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → ConstituentActions' },
312
- ] },
313
- // Gift sub-resources
314
- { Name: 'GiftAttachments', DisplayName: 'Gift Attachment', Description: 'File attachments on gifts', SupportsWrite: true, Fields: [
315
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Attachment ID' },
316
- { Name: 'parent_id', DisplayName: 'Gift ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Gifts' },
317
- ] },
318
- // Fund/Appeal sub-resources
319
- { Name: 'FundCustomFields', DisplayName: 'Fund Custom Field', Description: 'Custom fields on funds', SupportsWrite: true, Fields: [
320
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom field ID' },
321
- { Name: 'parent_id', DisplayName: 'Fund ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Funds' },
322
- ] },
323
- { Name: 'AppealCustomFields', DisplayName: 'Appeal Custom Field', Description: 'Custom fields on appeals', SupportsWrite: true, Fields: [
324
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom field ID' },
325
- { Name: 'parent_id', DisplayName: 'Appeal ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Appeals' },
326
- ] },
327
- // RecurringGiftSchedules REMOVED — schedules are a sub-object property of recurring gifts, not a separate endpoint (verified)
328
- // Event sub-resources
329
- { Name: 'EventCategories', DisplayName: 'Event Category', Description: 'Event category definitions', SupportsWrite: false, Fields: [
330
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Category ID' },
331
- ] },
332
- { Name: 'EventFees', DisplayName: 'Event Fee', Description: 'Fee types on events', SupportsWrite: true, Fields: [
333
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Fee ID' },
334
- { Name: 'event_id', DisplayName: 'Event ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Events' },
335
- ] },
336
- { Name: 'EventAttachments', DisplayName: 'Event Attachment', Description: 'File attachments on events', SupportsWrite: true, Fields: [
337
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Attachment ID' },
338
- { Name: 'parent_id', DisplayName: 'Event ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Events' },
339
- ] },
340
- { Name: 'EventParticipantFees', DisplayName: 'Participant Fee', Description: 'Fees paid by event participants', SupportsWrite: true, Fields: [
341
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Participant fee ID' },
342
- { Name: 'participant_id', DisplayName: 'Participant ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → EventParticipants' },
343
- ] },
344
- // Opportunity sub-resources
345
- { Name: 'OpportunityCustomFields', DisplayName: 'Opportunity Custom Field', Description: 'Custom fields on opportunities', SupportsWrite: true, Fields: [
346
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom field ID' },
347
- { Name: 'parent_id', DisplayName: 'Opportunity ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Opportunities' },
348
- ] },
349
- { Name: 'OpportunityFundraisers', DisplayName: 'Opportunity Fundraiser', Description: 'Fundraisers assigned to opportunities', SupportsWrite: true, Fields: [
350
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Assignment ID' },
351
- { Name: 'opportunity_id', DisplayName: 'Opportunity ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Opportunities' },
352
- ] },
353
- // Lists sub-resource
354
- { Name: 'ListConstituents', DisplayName: 'List Member', Description: 'Constituents in a list', SupportsWrite: true, Fields: [
355
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Constituent ID in list' },
356
- { Name: 'list_id', DisplayName: 'List ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Lists' },
357
- ] },
358
- // Tribute sub-resources
359
- { Name: 'TributeTypes', DisplayName: 'Tribute Type', Description: 'Tribute type lookup', SupportsWrite: false, Fields: [
360
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
361
- ] },
362
- { Name: 'TributeAcknowledgees', DisplayName: 'Tribute Acknowledgee', Description: 'Acknowledgee records on tributes', SupportsWrite: true, Fields: [
363
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Acknowledgee ID' },
364
- { Name: 'tribute_id', DisplayName: 'Tribute ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Tributes' },
365
- ] },
366
- // GL sub-resources
367
- { Name: 'GLProjects', DisplayName: 'GL Project', Description: 'General ledger projects', SupportsWrite: true, Fields: [
368
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Project ID' },
369
- ] },
370
- { Name: 'GLJournalEntries', DisplayName: 'GL Journal Entry', Description: 'GL journal entries', SupportsWrite: true, Fields: [
371
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'JE ID' },
372
- { Name: 'post_date', DisplayName: 'Post Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Incremental date' },
373
- ] },
374
- { Name: 'GLFiscalYears', DisplayName: 'Fiscal Year', Description: 'Fiscal year definitions', SupportsWrite: false, Fields: [
375
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'FY ID' },
376
- ] },
377
- // Communication preferences
378
- { Name: 'CommunicationPreferences', DisplayName: 'Comm Preference', Description: 'Communication preferences per constituent', SupportsWrite: true, Fields: [
379
- { Name: 'constituent_id', DisplayName: 'Constituent ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'FK → Constituents' },
380
- ] },
381
- // ConsentChannels REMOVED — not a standalone endpoint, embedded in communication preferences model (verified)
382
- // Remaining lookups
383
- { Name: 'ConstituentCodeTypes', DisplayName: 'Code Type', Description: 'Constituent code type definitions', SupportsWrite: false, Fields: [
384
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
385
- ] },
386
- { Name: 'CustomFieldCategories', DisplayName: 'CF Category', Description: 'Custom field category definitions', SupportsWrite: false, Fields: [
387
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Category ID' },
388
- ] },
389
- { Name: 'SuffixCodes', DisplayName: 'Suffix Code', Description: 'Name suffix lookup', SupportsWrite: false, Fields: [
390
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Code ID' },
391
- ] },
392
- { Name: 'TitleCodes', DisplayName: 'Title Code', Description: 'Name title/prefix lookup', SupportsWrite: false, Fields: [
393
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Code ID' },
394
- ] },
395
- { Name: 'GiftSubtypes', DisplayName: 'Gift Subtype', Description: 'Gift subtype lookup', SupportsWrite: false, Fields: [
396
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Subtype ID' },
397
- ] },
398
- { Name: 'OpportunityTypes', DisplayName: 'Opportunity Type', Description: 'Opportunity type lookup', SupportsWrite: false, Fields: [
399
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
400
- ] },
401
- { Name: 'ActionStatusTypes', DisplayName: 'Action Status', Description: 'Action status type lookup', SupportsWrite: false, Fields: [
402
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Status ID' },
403
- ] },
404
- { Name: 'EventFeeTypes', DisplayName: 'Event Fee Type', Description: 'Event fee type lookup', SupportsWrite: false, Fields: [
405
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Type ID' },
406
- ] },
407
- { Name: 'MembershipStanding', DisplayName: 'Membership Standing', Description: 'Membership standing lookup', SupportsWrite: false, Fields: [
408
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Standing ID' },
409
- ] },
410
- { Name: 'MembershipDues', DisplayName: 'Membership Dues', Description: 'Dues records per membership', SupportsWrite: false, Fields: [
411
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Dues ID' },
412
- ] },
413
- // Remaining sub-resources from audit
414
- { Name: 'PledgeCustomFields', DisplayName: 'Pledge Custom Field', Description: 'Custom fields on pledges', SupportsWrite: false, Fields: [
415
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'CF ID' },
416
- { Name: 'parent_id', DisplayName: 'Pledge ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Pledges' },
417
- ] },
418
- { Name: 'OpportunityAttachments', DisplayName: 'Opportunity Attachment', Description: 'File attachments on opportunities', SupportsWrite: true, Fields: [
419
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Attachment ID' },
420
- { Name: 'parent_id', DisplayName: 'Opportunity ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → Opportunities' },
421
- ] },
422
- { Name: 'OpportunityStatus', DisplayName: 'Opportunity Status', Description: 'Opportunity status lookup', SupportsWrite: false, Fields: [
423
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Status ID' },
424
- ] },
425
- // ConsentCategories REMOVED — not a standalone endpoint, embedded in communication preferences model (verified)
426
- { Name: 'EventParticipantDonations', DisplayName: 'Participant Donation', Description: 'Donations from event participants', SupportsWrite: false, Fields: [
427
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Donation ID' },
428
- { Name: 'participant_id', DisplayName: 'Participant ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'FK → EventParticipants' },
429
- ] },
430
- ];
431
- // ─── Connector ────────────────────────────────────────────────────────────
432
- let BlackbaudConnector = class BlackbaudConnector extends BaseRESTIntegrationConnector {
433
- constructor() {
434
- super(...arguments);
435
- this.tokenCache = null;
436
- this.lastRequestTime = 0;
437
- }
438
- get IntegrationName() { return 'Blackbaud'; }
439
- get SupportsCreate() { return true; }
440
- get SupportsUpdate() { return true; }
441
- get SupportsDelete() { return true; }
442
- GetIntegrationObjects() {
443
- return BB_OBJECTS;
444
- }
445
- GetActionGeneratorConfig() {
446
- const config = super.GetActionGeneratorConfig();
447
- if (!config)
448
- return null;
449
- config.IconClass = 'fa-solid fa-hand-holding-heart';
450
- config.CategoryDescription = 'Blackbaud Raiser\'s Edge NXT for constituents, gifts, campaigns, and fundraising';
451
- config.ParentCategoryName = 'Nonprofit/Fundraising';
452
- config.IncludeSearch = true;
453
- config.IncludeList = true;
454
- return config;
455
- }
456
- // ─── Discovery ─────────────────────────────────────────────────────
457
- async DiscoverObjects(_companyIntegration, _contextUser) {
458
- return BB_OBJECTS.map(obj => ({
459
- Name: obj.Name,
460
- Label: obj.DisplayName,
461
- Description: obj.Description,
462
- SupportsIncrementalSync: ['Constituents', 'Gifts'].includes(obj.Name),
463
- SupportsWrite: obj.SupportsWrite ?? false,
464
- }));
465
- }
466
- async DiscoverFields(companyIntegration, objectName, contextUser) {
467
- try {
468
- const auth = await this.Authenticate(companyIntegration, contextUser);
469
- const headers = this.BuildHeaders(auth);
470
- const apiPath = objectName.replace(/([a-z])([A-Z])/g, '$1/$2').toLowerCase();
471
- const response = await this.MakeHTTPRequest(auth, `${BB_API_BASE}/constituent/v1/${apiPath}?limit=1`, 'GET', headers);
472
- if (response.Status === 200) {
473
- const records = this.NormalizeResponse(response.Body, null);
474
- if (records.length > 0)
475
- return this.InferFieldsWithOverlay(records[0], objectName, BB_OBJECTS);
476
- }
477
- }
478
- catch { /* fall through */ }
479
- const staticObj = BB_OBJECTS.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
480
- if (!staticObj)
481
- return [];
482
- return staticObj.Fields.map(f => ({ Name: f.Name, Label: f.DisplayName, Description: f.Description, DataType: f.Type, IsRequired: f.IsRequired, IsUniqueKey: f.IsPrimaryKey, IsReadOnly: f.IsReadOnly }));
483
- }
484
- InferFieldsWithOverlay(sample, objectName, allObjects) {
485
- const staticObj = allObjects.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
486
- const staticMap = new Map((staticObj?.Fields ?? []).map(f => [f.Name.toLowerCase(), f]));
487
- const fields = [];
488
- for (const [key, value] of Object.entries(sample)) {
489
- if (key === 'links' || key === '_links')
490
- continue;
491
- const sf = staticMap.get(key.toLowerCase());
492
- fields.push({ Name: key, Label: sf?.DisplayName ?? key, Description: sf?.Description ?? '',
493
- DataType: sf?.Type ?? this.InferTypeVal(value), IsRequired: sf?.IsRequired ?? false, IsUniqueKey: sf?.IsPrimaryKey ?? false, IsReadOnly: sf?.IsReadOnly ?? false });
494
- }
495
- return fields;
496
- }
497
- InferTypeVal(v) {
498
- if (v === null || v === undefined)
499
- return 'string';
500
- if (typeof v === 'number')
501
- return Number.isInteger(v) ? 'number' : 'decimal';
502
- if (typeof v === 'boolean')
503
- return 'boolean';
504
- if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}/.test(v))
505
- return 'datetime';
506
- return 'string';
507
- }
508
- // ─── Auth ──────────────────────────────────────────────────────────
509
- async Authenticate(companyIntegration, contextUser) {
510
- const config = await this.ParseConfig(companyIntegration, contextUser);
511
- if (this.tokenCache && this.tokenCache.ExpiresAt > Date.now() + TOKEN_REFRESH_BUFFER_MS) {
512
- return {
513
- Token: this.tokenCache.AccessToken,
514
- TokenType: 'Bearer',
515
- Config: config,
516
- SubscriptionKey: config.SubscriptionKey,
517
- };
518
- }
519
- const refreshToken = this.tokenCache?.RefreshToken ?? config.RefreshToken;
520
- if (refreshToken) {
521
- try {
522
- const refreshed = await this.RefreshToken(config, refreshToken);
523
- this.tokenCache = refreshed;
524
- return {
525
- Token: refreshed.AccessToken,
526
- TokenType: 'Bearer',
527
- Config: config,
528
- SubscriptionKey: config.SubscriptionKey,
529
- };
530
- }
531
- catch {
532
- // Fall through to use stored access token
533
- }
534
- }
535
- this.tokenCache = {
536
- AccessToken: config.AccessToken,
537
- RefreshToken: config.RefreshToken,
538
- ExpiresAt: Date.now() + (3600 * 1000),
539
- };
540
- return {
541
- Token: config.AccessToken,
542
- TokenType: 'Bearer',
543
- Config: config,
544
- SubscriptionKey: config.SubscriptionKey,
545
- };
546
- }
547
- async RefreshToken(config, refreshToken) {
548
- const basicAuth = Buffer.from(`${config.ClientID}:${config.ClientSecret}`).toString('base64');
549
- const body = new URLSearchParams({
550
- grant_type: 'refresh_token',
551
- refresh_token: refreshToken,
552
- });
553
- const response = await fetch(BB_TOKEN_URL, {
554
- method: 'POST',
555
- headers: {
556
- 'Authorization': `Basic ${basicAuth}`,
557
- 'Content-Type': 'application/x-www-form-urlencoded',
558
- },
559
- body: body.toString(),
560
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
561
- });
562
- if (!response.ok) {
563
- throw new Error(`Blackbaud token refresh failed: ${response.status}`);
564
- }
565
- const data = await response.json();
566
- return {
567
- AccessToken: data.access_token,
568
- RefreshToken: data.refresh_token ?? refreshToken,
569
- ExpiresAt: Date.now() + ((data.expires_in ?? 3600) * 1000),
570
- };
571
- }
572
- async ParseConfig(companyIntegration, contextUser, provider) {
573
- const credentialID = companyIntegration.CredentialID;
574
- if (credentialID) {
575
- const md = provider ?? new Metadata();
576
- const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
577
- const loaded = await credential.Load(credentialID);
578
- if (loaded && credential.Values) {
579
- const parsed = JSON.parse(credential.Values);
580
- if (parsed['ClientID'] && parsed['SubscriptionKey']) {
581
- return {
582
- ClientID: parsed['ClientID'],
583
- ClientSecret: parsed['ClientSecret'] ?? '',
584
- SubscriptionKey: parsed['SubscriptionKey'],
585
- AccessToken: parsed['AccessToken'] ?? '',
586
- RefreshToken: parsed['RefreshToken'] ?? '',
587
- };
588
- }
589
- }
590
- }
591
- const configJson = companyIntegration.Configuration;
592
- if (configJson) {
593
- const parsed = JSON.parse(configJson);
594
- return {
595
- ClientID: parsed['ClientID'] ?? '',
596
- ClientSecret: parsed['ClientSecret'] ?? '',
597
- SubscriptionKey: parsed['SubscriptionKey'] ?? '',
598
- AccessToken: parsed['AccessToken'] ?? '',
599
- RefreshToken: parsed['RefreshToken'] ?? '',
600
- };
601
- }
602
- throw new Error('No Blackbaud credentials found. Set ClientID, ClientSecret, SubscriptionKey, AccessToken, RefreshToken in credential Values or Configuration JSON.');
603
- }
604
- // ─── TestConnection ────────────────────────────────────────────────
605
- async TestConnection(companyIntegration, contextUser) {
606
- try {
607
- const auth = await this.Authenticate(companyIntegration, contextUser);
608
- const headers = this.BuildHeaders(auth);
609
- const url = `${BB_API_BASE}/constituent/v1/constituents?limit=1`;
610
- const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
611
- if (response.Status === 200) {
612
- const body = response.Body;
613
- return { Success: true, Message: `Connected to Blackbaud SKY API — ${body.count ?? 0} total constituents` };
614
- }
615
- return { Success: false, Message: `API returned ${response.Status}` };
616
- }
617
- catch (err) {
618
- return { Success: false, Message: err instanceof Error ? err.message : String(err) };
619
- }
620
- }
621
- // ─── URL Building ──────────────────────────────────────────────────
622
- GetBaseURL(_companyIntegration, _auth) {
623
- return BB_API_BASE;
624
- }
625
- BuildPaginatedURL(basePath, _obj, _page, offset, _cursor) {
626
- const sep = basePath.includes('?') ? '&' : '?';
627
- return `${basePath}${sep}limit=${BB_PAGE_SIZE}&offset=${offset}`;
628
- }
629
- // ─── Response Parsing ──────────────────────────────────────────────
630
- NormalizeResponse(rawBody, _responseDataKey) {
631
- if (Array.isArray(rawBody))
632
- return rawBody;
633
- const body = rawBody;
634
- if (Array.isArray(body['value']))
635
- return body['value'];
636
- if (body && Object.keys(body).length > 0)
637
- return [body];
638
- return [];
639
- }
640
- ExtractPaginationInfo(rawBody, _paginationType, _currentPage, currentOffset, _pageSize) {
641
- const body = rawBody;
642
- const records = this.NormalizeResponse(rawBody, null);
643
- const hasMore = !!body.next_link;
644
- return { HasMore: hasMore, NextOffset: currentOffset + records.length };
645
- }
646
- // ─── FetchChanges ──────────────────────────────────────────────────
647
- async FetchChanges(ctx) {
648
- const objectLower = ctx.ObjectName.toLowerCase();
649
- switch (objectLower) {
650
- case 'constituents': return this.FetchConstituents(ctx);
651
- case 'constituentaddresses': return this.FetchConstituentChildren(ctx, 'addresses', 'ConstituentAddresses');
652
- case 'constituentphones': return this.FetchConstituentChildren(ctx, 'phones', 'ConstituentPhones');
653
- case 'constituentemails': return this.FetchConstituentChildren(ctx, 'emailaddresses', 'ConstituentEmails');
654
- case 'gifts': return this.FetchGifts(ctx);
655
- case 'fundraisers': return this.FetchList(ctx, `${BB_API_BASE}/fundraising/v1/fundraisers`, 'Fundraisers');
656
- case 'campaigns': return this.FetchList(ctx, `${BB_API_BASE}/fundraising/v1/campaigns`, 'Campaigns');
657
- case 'opportunities': return this.FetchList(ctx, `${BB_API_BASE}/opportunity/v1/opportunities`, 'Opportunities');
658
- default: return this.FetchGenericBBObject(ctx);
659
- }
660
- }
661
- async FetchGenericBBObject(ctx) {
662
- // Generic fetch — maps object name to likely SKY API path
663
- const apiPath = ctx.ObjectName.replace(/([a-z])([A-Z])/g, '$1/$2').toLowerCase();
664
- const url = `${BB_API_BASE}/constituent/v1/${apiPath}`;
665
- return this.FetchList(ctx, url, ctx.ObjectName);
666
- }
667
- async FetchConstituents(ctx) {
668
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
669
- const headers = this.BuildHeaders(auth);
670
- const offset = ctx.CurrentOffset ?? 0;
671
- let url = `${BB_API_BASE}/constituent/v1/constituents?limit=${BB_PAGE_SIZE}&offset=${offset}`;
672
- if (ctx.WatermarkValue) {
673
- url += `&date_modified=${encodeURIComponent(ctx.WatermarkValue)}`;
674
- }
675
- return this.ExecuteListFetch(auth, headers, url, 'Constituents', 'id', offset, ctx);
676
- }
677
- async FetchGifts(ctx) {
678
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
679
- const headers = this.BuildHeaders(auth);
680
- const offset = ctx.CurrentOffset ?? 0;
681
- let url = `${BB_API_BASE}/gift/v1/gifts?limit=${BB_PAGE_SIZE}&offset=${offset}`;
682
- if (ctx.WatermarkValue) {
683
- url += `&date_modified=${encodeURIComponent(ctx.WatermarkValue)}`;
684
- }
685
- return this.ExecuteListFetch(auth, headers, url, 'Gifts', 'id', offset, ctx);
686
- }
687
- async FetchList(ctx, endpoint, objectType) {
688
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
689
- const headers = this.BuildHeaders(auth);
690
- const offset = ctx.CurrentOffset ?? 0;
691
- let url = `${endpoint}?limit=${BB_PAGE_SIZE}&offset=${offset}`;
692
- // Blackbaud SKY API endpoint families that document a `date_modified`
693
- // query filter: Constituent API + Gift API + Fundraising API +
694
- // Opportunity API. The connector identifies them by URL substring so the
695
- // metadata can flip SupportsIncrementalSync=true for IOs in those
696
- // families and FetchList will honor the watermark.
697
- if (ctx.WatermarkValue && this.SupportsDateModified(endpoint)) {
698
- url += `&date_modified=${encodeURIComponent(ctx.WatermarkValue)}`;
699
- }
700
- return this.ExecuteListFetch(auth, headers, url, objectType, 'id', offset, ctx);
701
- }
702
- /**
703
- * Returns true if a Blackbaud SKY API endpoint is in a family documented to
704
- * accept the `date_modified` query filter. Conservative — only includes
705
- * endpoint roots where the SKY API docs surface the filter. Add new prefixes
706
- * after verifying against the vendor docs for that endpoint family.
707
- */
708
- SupportsDateModified(endpoint) {
709
- const supportedPrefixes = [
710
- '/constituent/v1/',
711
- '/gift/v1/',
712
- '/fundraising/v1/',
713
- '/opportunity/v1/',
714
- ];
715
- return supportedPrefixes.some(p => endpoint.includes(p));
716
- }
717
- async FetchConstituentChildren(ctx, childEndpoint, objectType) {
718
- // Blackbaud provides flat list endpoints for child resources — no need to iterate parents
719
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
720
- const headers = this.BuildHeaders(auth);
721
- const offset = ctx.CurrentOffset ?? 0;
722
- let url = `${BB_API_BASE}/constituent/v1/constituents/${childEndpoint}?limit=${BB_PAGE_SIZE}&offset=${offset}`;
723
- // Constituent-child endpoints are in the Constituent API family —
724
- // honor the watermark when provided.
725
- if (ctx.WatermarkValue) {
726
- url += `&date_modified=${encodeURIComponent(ctx.WatermarkValue)}`;
727
- }
728
- return this.ExecuteListFetch(auth, headers, url, objectType, 'id', offset, ctx);
729
- }
730
- async ExecuteListFetch(auth, headers, url, objectType, pkField, offset, ctx) {
731
- const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
732
- if (response.Status < 200 || response.Status >= 300) {
733
- throw new Error(`Blackbaud ${objectType} API error: ${response.Status}`);
734
- }
735
- const body = response.Body;
736
- const records = this.NormalizeResponse(response.Body, null);
737
- const externalRecords = records.map(r => ({
738
- ExternalID: String(r[pkField] ?? ''),
739
- ObjectType: objectType,
740
- Fields: r,
741
- }));
742
- const hasMore = !!body.next_link;
743
- let newWatermark;
744
- if (!hasMore) {
745
- for (const r of records) {
746
- const modified = r['date_modified'];
747
- if (modified && (!newWatermark || modified > newWatermark))
748
- newWatermark = modified;
749
- }
750
- }
751
- return {
752
- Records: externalRecords,
753
- HasMore: hasMore,
754
- NextOffset: offset + records.length,
755
- NewWatermarkValue: !hasMore ? newWatermark : undefined,
756
- };
757
- }
758
- // ─── CRUD ──────────────────────────────────────────────────────────
759
- async CreateRecord(ctx) {
760
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
761
- const headers = { ...this.BuildHeaders(auth), 'Content-Type': 'application/json' };
762
- const url = this.CRUDEndpointURL(ctx.ObjectName, ctx.Attributes, null);
763
- const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, ctx.Attributes);
764
- if (response.Status >= 200 && response.Status < 300) {
765
- const body = response.Body;
766
- return this.BuildCreatedResult(String(body['id'] ?? ''), response.Status, ctx.ObjectName);
767
- }
768
- return this.BuildCRUDError(response, 'CreateRecord', ctx.ObjectName);
769
- }
770
- async UpdateRecord(ctx) {
771
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
772
- const headers = { ...this.BuildHeaders(auth), 'Content-Type': 'application/json' };
773
- const url = this.CRUDEndpointURL(ctx.ObjectName, ctx.Attributes, ctx.ExternalID);
774
- const response = await this.MakeHTTPRequest(auth, url, 'PATCH', headers, ctx.Attributes);
775
- if (response.Status >= 200 && response.Status < 300) {
776
- return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
777
- }
778
- return this.BuildCRUDError(response, 'UpdateRecord', ctx.ObjectName);
779
- }
780
- async DeleteRecord(ctx) {
781
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
782
- const headers = this.BuildHeaders(auth);
783
- const url = this.CRUDEndpointURL(ctx.ObjectName, {}, ctx.ExternalID);
784
- const response = await this.MakeHTTPRequest(auth, url, 'DELETE', headers);
785
- if (response.Status === 204 || (response.Status >= 200 && response.Status < 300)) {
786
- return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
787
- }
788
- return this.BuildCRUDError(response, 'DeleteRecord', ctx.ObjectName);
789
- }
790
- CRUDEndpointURL(objectName, attrs, externalID) {
791
- const lower = objectName.toLowerCase();
792
- if (lower === 'constituentaddresses') {
793
- const base = `${BB_API_BASE}/constituent/v1/addresses`;
794
- return externalID ? `${base}/${externalID}` : base;
795
- }
796
- if (lower === 'constituentphones') {
797
- const base = `${BB_API_BASE}/constituent/v1/phones`;
798
- return externalID ? `${base}/${externalID}` : base;
799
- }
800
- if (lower === 'constituentemails') {
801
- const base = `${BB_API_BASE}/constituent/v1/emailaddresses`;
802
- return externalID ? `${base}/${externalID}` : base;
803
- }
804
- const endpointMap = {
805
- 'constituents': `${BB_API_BASE}/constituent/v1/constituents`,
806
- 'gifts': `${BB_API_BASE}/gift/v1/gifts`,
807
- };
808
- const base = endpointMap[lower] ?? `${BB_API_BASE}/${lower}`;
809
- return externalID ? `${base}/${externalID}` : base;
810
- }
811
- BuildCRUDError(response, operation, objectName) {
812
- const bodyStr = typeof response.Body === 'string' ? response.Body : JSON.stringify(response.Body);
813
- return {
814
- Success: false,
815
- ExternalID: '',
816
- StatusCode: response.Status,
817
- ErrorMessage: `${operation} failed for ${objectName}: HTTP ${response.Status} — ${bodyStr?.substring(0, 300)}`,
818
- };
819
- }
820
- // ─── Headers ───────────────────────────────────────────────────────
821
- BuildHeaders(auth) {
822
- const bbAuth = auth;
823
- return {
824
- 'Authorization': `Bearer ${auth.Token}`,
825
- 'bb-api-subscription-key': bbAuth.SubscriptionKey,
826
- 'Accept': 'application/json',
827
- 'User-Agent': 'MemberJunction-Integration/1.0',
828
- };
829
- }
830
- // ─── HTTP Transport ────────────────────────────────────────────────
831
- async MakeHTTPRequest(_auth, url, method, headers, body) {
832
- await this.Throttle();
833
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
834
- const response = await this.FetchWithTimeout(url, method, headers, body);
835
- this.lastRequestTime = Date.now();
836
- if (response.status === 401 && attempt === 0) {
837
- this.tokenCache = null;
838
- console.warn('[Blackbaud] 401 — clearing token cache for retry');
839
- continue;
840
- }
841
- if (response.status === 429) {
842
- const retryAfter = parseInt(response.headers.get('Retry-After') ?? '10', 10);
843
- const delay = Math.min(retryAfter * 1000, 60_000);
844
- console.warn(`[Blackbaud] Rate limited (429), waiting ${delay}ms`);
845
- await this.Sleep(delay);
846
- continue;
847
- }
848
- if (response.status >= 500 && attempt < MAX_RETRIES) {
849
- const delay = Math.min(1000 * Math.pow(2, attempt), 30_000);
850
- console.warn(`[Blackbaud] Server error ${response.status}, retrying in ${delay}ms`);
851
- await this.Sleep(delay);
852
- continue;
853
- }
854
- const responseBody = await this.ParseBody(response);
855
- return this.ToRESTResponse(response, responseBody);
856
- }
857
- throw new Error(`Blackbaud request failed after ${MAX_RETRIES} retries: ${url}`);
858
- }
859
- async FetchWithTimeout(url, method, headers, body) {
860
- const controller = new AbortController();
861
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
862
- try {
863
- const opts = { method, headers, signal: controller.signal };
864
- if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
865
- opts.body = JSON.stringify(body);
866
- }
867
- return await fetch(url, opts);
868
- }
869
- catch (err) {
870
- if (err instanceof Error && err.name === 'AbortError') {
871
- throw new Error(`Blackbaud request timed out: ${url}`);
872
- }
873
- throw err;
874
- }
875
- finally {
876
- clearTimeout(timeoutId);
877
- }
878
- }
879
- async ParseBody(response) {
880
- const ct = response.headers.get('content-type') ?? '';
881
- if (ct.includes('json'))
882
- return response.json();
883
- return response.text();
884
- }
885
- ToRESTResponse(response, body) {
886
- const hdrs = {};
887
- response.headers.forEach((v, k) => { hdrs[k.toLowerCase()] = v; });
888
- return { Status: response.status, Body: body, Headers: hdrs };
889
- }
890
- async Throttle() {
891
- const elapsed = Date.now() - this.lastRequestTime;
892
- if (elapsed < MIN_REQUEST_INTERVAL_MS)
893
- await this.Sleep(MIN_REQUEST_INTERVAL_MS - elapsed);
894
- }
895
- Sleep(ms) {
896
- return new Promise(resolve => setTimeout(resolve, ms));
897
- }
898
- // ─── Default Field Mappings ────────────────────────────────────────
899
- GetDefaultFieldMappings(objectName) {
900
- const obj = BB_OBJECTS.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
901
- if (!obj)
902
- return [];
903
- return obj.Fields.map(f => ({
904
- SourceFieldName: f.Name,
905
- DestinationFieldName: f.Name,
906
- IsKeyField: f.IsPrimaryKey,
907
- }));
908
- }
909
- };
910
- BlackbaudConnector = __decorate([
911
- RegisterClass(BaseIntegrationConnector, 'BlackbaudConnector')
912
- ], BlackbaudConnector);
913
- export { BlackbaudConnector };
914
- // Tree-shaking prevention — REQUIRED
915
- export function LoadBlackbaudConnector() { }
916
- //# sourceMappingURL=BlackbaudConnector.js.map