@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.
- package/LICENSE +7 -0
- package/README.md +109 -277
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -38
- package/dist/index.js.map +1 -1
- package/package.json +14 -19
- package/dist/AptifyConnector.d.ts +0 -95
- package/dist/AptifyConnector.d.ts.map +0 -1
- package/dist/AptifyConnector.js +0 -760
- package/dist/AptifyConnector.js.map +0 -1
- package/dist/BlackbaudConnector.d.ts +0 -64
- package/dist/BlackbaudConnector.d.ts.map +0 -1
- package/dist/BlackbaudConnector.js +0 -916
- package/dist/BlackbaudConnector.js.map +0 -1
- package/dist/ConstantContactConnector.d.ts +0 -137
- package/dist/ConstantContactConnector.d.ts.map +0 -1
- package/dist/ConstantContactConnector.js +0 -663
- package/dist/ConstantContactConnector.js.map +0 -1
- package/dist/CventConnector.d.ts +0 -146
- package/dist/CventConnector.d.ts.map +0 -1
- package/dist/CventConnector.js +0 -540
- package/dist/CventConnector.js.map +0 -1
- package/dist/DynamicsDataverseConnector.d.ts +0 -296
- package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
- package/dist/DynamicsDataverseConnector.js +0 -790
- package/dist/DynamicsDataverseConnector.js.map +0 -1
- package/dist/FileFeedConnector.d.ts +0 -64
- package/dist/FileFeedConnector.d.ts.map +0 -1
- package/dist/FileFeedConnector.js +0 -190
- package/dist/FileFeedConnector.js.map +0 -1
- package/dist/FontevaConnector.d.ts +0 -153
- package/dist/FontevaConnector.d.ts.map +0 -1
- package/dist/FontevaConnector.js +0 -421
- package/dist/FontevaConnector.js.map +0 -1
- package/dist/GrowthZoneConnector.d.ts +0 -183
- package/dist/GrowthZoneConnector.d.ts.map +0 -1
- package/dist/GrowthZoneConnector.js +0 -702
- package/dist/GrowthZoneConnector.js.map +0 -1
- package/dist/HivebriteConnector.d.ts +0 -151
- package/dist/HivebriteConnector.d.ts.map +0 -1
- package/dist/HivebriteConnector.js +0 -533
- package/dist/HivebriteConnector.js.map +0 -1
- package/dist/HubSpotConnector.d.ts +0 -523
- package/dist/HubSpotConnector.d.ts.map +0 -1
- package/dist/HubSpotConnector.js +0 -3110
- package/dist/HubSpotConnector.js.map +0 -1
- package/dist/IMISConnector.d.ts +0 -171
- package/dist/IMISConnector.d.ts.map +0 -1
- package/dist/IMISConnector.js +0 -695
- package/dist/IMISConnector.js.map +0 -1
- package/dist/MJToMJConnector.d.ts +0 -279
- package/dist/MJToMJConnector.d.ts.map +0 -1
- package/dist/MJToMJConnector.js +0 -893
- package/dist/MJToMJConnector.js.map +0 -1
- package/dist/MagnetMailConnector.d.ts +0 -163
- package/dist/MagnetMailConnector.d.ts.map +0 -1
- package/dist/MagnetMailConnector.js +0 -823
- package/dist/MagnetMailConnector.js.map +0 -1
- package/dist/MailchimpConnector.d.ts +0 -197
- package/dist/MailchimpConnector.d.ts.map +0 -1
- package/dist/MailchimpConnector.js +0 -791
- package/dist/MailchimpConnector.js.map +0 -1
- package/dist/MemberSuiteConnector.d.ts +0 -185
- package/dist/MemberSuiteConnector.d.ts.map +0 -1
- package/dist/MemberSuiteConnector.js +0 -762
- package/dist/MemberSuiteConnector.js.map +0 -1
- package/dist/NeonCRMConnector.d.ts +0 -273
- package/dist/NeonCRMConnector.d.ts.map +0 -1
- package/dist/NeonCRMConnector.js +0 -1039
- package/dist/NeonCRMConnector.js.map +0 -1
- package/dist/NetForumConnector.d.ts +0 -170
- package/dist/NetForumConnector.d.ts.map +0 -1
- package/dist/NetForumConnector.js +0 -778
- package/dist/NetForumConnector.js.map +0 -1
- package/dist/NetSuiteConnector.d.ts +0 -136
- package/dist/NetSuiteConnector.d.ts.map +0 -1
- package/dist/NetSuiteConnector.js +0 -703
- package/dist/NetSuiteConnector.js.map +0 -1
- package/dist/NimbleAMSConnector.d.ts +0 -142
- package/dist/NimbleAMSConnector.d.ts.map +0 -1
- package/dist/NimbleAMSConnector.js +0 -703
- package/dist/NimbleAMSConnector.js.map +0 -1
- package/dist/NoviConnector.d.ts +0 -178
- package/dist/NoviConnector.d.ts.map +0 -1
- package/dist/NoviConnector.js +0 -566
- package/dist/NoviConnector.js.map +0 -1
- package/dist/ORCIDConnector.d.ts +0 -207
- package/dist/ORCIDConnector.d.ts.map +0 -1
- package/dist/ORCIDConnector.js +0 -708
- package/dist/ORCIDConnector.js.map +0 -1
- package/dist/OpenWaterConnector.d.ts +0 -173
- package/dist/OpenWaterConnector.d.ts.map +0 -1
- package/dist/OpenWaterConnector.js +0 -893
- package/dist/OpenWaterConnector.js.map +0 -1
- package/dist/PathLMSConnector.d.ts +0 -395
- package/dist/PathLMSConnector.d.ts.map +0 -1
- package/dist/PathLMSConnector.js +0 -1281
- package/dist/PathLMSConnector.js.map +0 -1
- package/dist/PheedLoopConnector.d.ts +0 -119
- package/dist/PheedLoopConnector.d.ts.map +0 -1
- package/dist/PheedLoopConnector.js +0 -429
- package/dist/PheedLoopConnector.js.map +0 -1
- package/dist/PropFuelConnector.d.ts +0 -153
- package/dist/PropFuelConnector.d.ts.map +0 -1
- package/dist/PropFuelConnector.js +0 -450
- package/dist/PropFuelConnector.js.map +0 -1
- package/dist/QuickBooksConnector.d.ts +0 -92
- package/dist/QuickBooksConnector.d.ts.map +0 -1
- package/dist/QuickBooksConnector.js +0 -713
- package/dist/QuickBooksConnector.js.map +0 -1
- package/dist/RasaConnector.d.ts +0 -152
- package/dist/RasaConnector.d.ts.map +0 -1
- package/dist/RasaConnector.js +0 -929
- package/dist/RasaConnector.js.map +0 -1
- package/dist/Reach360Connector.d.ts +0 -175
- package/dist/Reach360Connector.d.ts.map +0 -1
- package/dist/Reach360Connector.js +0 -796
- package/dist/Reach360Connector.js.map +0 -1
- package/dist/RelationalDBConnector.d.ts +0 -108
- package/dist/RelationalDBConnector.d.ts.map +0 -1
- package/dist/RelationalDBConnector.js +0 -228
- package/dist/RelationalDBConnector.js.map +0 -1
- package/dist/RhythmConnector.d.ts +0 -188
- package/dist/RhythmConnector.d.ts.map +0 -1
- package/dist/RhythmConnector.js +0 -784
- package/dist/RhythmConnector.js.map +0 -1
- package/dist/SageIntacctConnector.d.ts +0 -337
- package/dist/SageIntacctConnector.d.ts.map +0 -1
- package/dist/SageIntacctConnector.js +0 -2081
- package/dist/SageIntacctConnector.js.map +0 -1
- package/dist/SalesforceConnector.d.ts +0 -461
- package/dist/SalesforceConnector.d.ts.map +0 -1
- package/dist/SalesforceConnector.js +0 -1862
- package/dist/SalesforceConnector.js.map +0 -1
- package/dist/SharePointConnector.d.ts +0 -188
- package/dist/SharePointConnector.d.ts.map +0 -1
- package/dist/SharePointConnector.js +0 -716
- package/dist/SharePointConnector.js.map +0 -1
- package/dist/WicketConnector.d.ts +0 -252
- package/dist/WicketConnector.d.ts.map +0 -1
- package/dist/WicketConnector.js +0 -938
- package/dist/WicketConnector.js.map +0 -1
- package/dist/WildApricotConnector.d.ts +0 -201
- package/dist/WildApricotConnector.d.ts.map +0 -1
- package/dist/WildApricotConnector.js +0 -1129
- package/dist/WildApricotConnector.js.map +0 -1
- package/dist/YourMembershipConnector.d.ts +0 -349
- package/dist/YourMembershipConnector.d.ts.map +0 -1
- package/dist/YourMembershipConnector.js +0 -4062
- package/dist/YourMembershipConnector.js.map +0 -1
- package/dist/generate-integration-actions.d.ts +0 -23
- package/dist/generate-integration-actions.d.ts.map +0 -1
- package/dist/generate-integration-actions.js +0 -201
- package/dist/generate-integration-actions.js.map +0 -1
package/dist/AptifyConnector.js
DELETED
|
@@ -1,760 +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
|
-
* AptifyConnector — REST integration connector for Aptify (Community Brands / Momentive Software)
|
|
9
|
-
* association management software.
|
|
10
|
-
*
|
|
11
|
-
* Reference: https://aptifysupport.zendesk.com/ (entity services + Aptify Web API docs are
|
|
12
|
-
* customer-portal gated; the public Web API root is /AptifyServicesAPI/services).
|
|
13
|
-
*
|
|
14
|
-
* Auth : Token-based. POST {BaseURL}/authentication with Username + Password
|
|
15
|
-
* (and optional AuthProvider, defaults to "AptifyUser") returns a TokenId GUID.
|
|
16
|
-
* Subsequent requests carry `Authorization: Web {TokenId}` (NOT a Bearer token).
|
|
17
|
-
* Base path : `${BaseURL}/services/{EntityName}` for generic CRUD on Aptify entities.
|
|
18
|
-
* Pagination: OData-style `$top` / `$skip` query parameters. List endpoints typically return
|
|
19
|
-
* a JSON array directly (or an envelope with an `Items` / `value` property).
|
|
20
|
-
* Increment : Filter `LastModifiedDate ge datetime'<iso>'` for entities that expose it.
|
|
21
|
-
* CRUD : Full Create / Read / Update / Delete via /services/{Entity}/{id}.
|
|
22
|
-
*
|
|
23
|
-
* NOT supported by this connector:
|
|
24
|
-
* - Aptify 7 Azure-native API (separate endpoint, different auth)
|
|
25
|
-
* - Bulk Import service (file-based, not API-driven)
|
|
26
|
-
* - Service Data Objects (business-logic wrappers — call the underlying entities instead)
|
|
27
|
-
*/
|
|
28
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
29
|
-
import { Metadata } from '@memberjunction/core';
|
|
30
|
-
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
31
|
-
// ─── Constants ──────────────────────────────────────────────────────────
|
|
32
|
-
const DEFAULT_PAGE_SIZE = 200;
|
|
33
|
-
const DEFAULT_MAX_RETRIES = 3;
|
|
34
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
35
|
-
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 200;
|
|
36
|
-
const TOKEN_REFRESH_BUFFER_MS = 60_000;
|
|
37
|
-
const TOKEN_LIFETIME_MS = 60 * 60 * 1000;
|
|
38
|
-
/**
|
|
39
|
-
* Static catalog of the Aptify entities this connector exposes for action generation
|
|
40
|
-
* and as the seed for runtime DiscoverFields. Field lists are intentionally sparse — the
|
|
41
|
-
* connector enriches them at runtime by fetching a sample record per entity and inferring
|
|
42
|
-
* the full property set. The static entries below pin primary keys and known foreign keys
|
|
43
|
-
* so downstream consumers (Action generator, Schema builder) get correct types even before
|
|
44
|
-
* a live API connection has been made.
|
|
45
|
-
*/
|
|
46
|
-
const APT_OBJECTS = [
|
|
47
|
-
{
|
|
48
|
-
Name: 'Persons', DisplayName: 'Person',
|
|
49
|
-
Description: 'Individual contacts and members. Aptify\'s primary identity entity.',
|
|
50
|
-
SupportsWrite: true,
|
|
51
|
-
Fields: [
|
|
52
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Person record ID' },
|
|
53
|
-
{ Name: 'LastModifiedDate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Used for incremental watermark' },
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
Name: 'Companies', DisplayName: 'Company',
|
|
58
|
-
Description: 'Organization records — employers, member orgs, vendors.',
|
|
59
|
-
SupportsWrite: true,
|
|
60
|
-
Fields: [
|
|
61
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Company record ID' },
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
Name: 'Memberships', DisplayName: 'Membership',
|
|
66
|
-
Description: 'Membership records linking Persons or Companies to MembershipTypes.',
|
|
67
|
-
SupportsWrite: true,
|
|
68
|
-
Fields: [
|
|
69
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Membership record ID' },
|
|
70
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
71
|
-
{ Name: 'CompanyId', DisplayName: 'Company ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Companies' },
|
|
72
|
-
{ Name: 'MembershipTypeId', DisplayName: 'Membership Type ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → MembershipTypes' },
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
Name: 'MembershipTypes', DisplayName: 'Membership Type',
|
|
77
|
-
Description: 'Definitions of membership categories (Individual, Corporate, Student, etc.).',
|
|
78
|
-
SupportsWrite: false,
|
|
79
|
-
Fields: [
|
|
80
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Membership type ID' },
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
Name: 'Events', DisplayName: 'Event',
|
|
85
|
-
Description: 'Events, meetings, conferences.',
|
|
86
|
-
SupportsWrite: true,
|
|
87
|
-
Fields: [
|
|
88
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Event record ID' },
|
|
89
|
-
],
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
Name: 'EventRegistrations', DisplayName: 'Event Registration',
|
|
93
|
-
Description: 'Person attendance records for Events.',
|
|
94
|
-
SupportsWrite: true,
|
|
95
|
-
Fields: [
|
|
96
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Registration record ID' },
|
|
97
|
-
{ Name: 'EventId', DisplayName: 'Event ID', Type: 'number', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Events' },
|
|
98
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
Name: 'Orders', DisplayName: 'Order',
|
|
103
|
-
Description: 'Sales / fulfillment order headers.',
|
|
104
|
-
SupportsWrite: true,
|
|
105
|
-
Fields: [
|
|
106
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Order record ID' },
|
|
107
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
108
|
-
{ Name: 'CompanyId', DisplayName: 'Company ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Companies' },
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
Name: 'OrderDetails', DisplayName: 'Order Detail',
|
|
113
|
-
Description: 'Order line items linking Products to Orders.',
|
|
114
|
-
SupportsWrite: true,
|
|
115
|
-
Fields: [
|
|
116
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Order detail record ID' },
|
|
117
|
-
{ Name: 'OrderId', DisplayName: 'Order ID', Type: 'number', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Orders' },
|
|
118
|
-
{ Name: 'ProductId', DisplayName: 'Product ID', Type: 'number', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Products' },
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
Name: 'Products', DisplayName: 'Product',
|
|
123
|
-
Description: 'Sellable products / SKUs.',
|
|
124
|
-
SupportsWrite: true,
|
|
125
|
-
Fields: [
|
|
126
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Product record ID' },
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
Name: 'Committees', DisplayName: 'Committee',
|
|
131
|
-
Description: 'Committees, boards, working groups.',
|
|
132
|
-
SupportsWrite: true,
|
|
133
|
-
Fields: [
|
|
134
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Committee record ID' },
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
Name: 'CommitteeMembers', DisplayName: 'Committee Member',
|
|
139
|
-
Description: 'Person assignments to Committees.',
|
|
140
|
-
SupportsWrite: true,
|
|
141
|
-
Fields: [
|
|
142
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Committee member record ID' },
|
|
143
|
-
{ Name: 'CommitteeId', DisplayName: 'Committee ID', Type: 'number', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Committees' },
|
|
144
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
145
|
-
],
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
Name: 'Addresses', DisplayName: 'Address',
|
|
149
|
-
Description: 'Postal addresses linked to Persons or Companies.',
|
|
150
|
-
SupportsWrite: true,
|
|
151
|
-
Fields: [
|
|
152
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Address record ID' },
|
|
153
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
154
|
-
{ Name: 'CompanyId', DisplayName: 'Company ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Companies' },
|
|
155
|
-
],
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
Name: 'Donations', DisplayName: 'Donation',
|
|
159
|
-
Description: 'Donations and gifts.',
|
|
160
|
-
SupportsWrite: true,
|
|
161
|
-
Fields: [
|
|
162
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Donation record ID' },
|
|
163
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
164
|
-
],
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
Name: 'Certifications', DisplayName: 'Certification',
|
|
168
|
-
Description: 'Professional certifications held by Persons.',
|
|
169
|
-
SupportsWrite: true,
|
|
170
|
-
Fields: [
|
|
171
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Certification record ID' },
|
|
172
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
173
|
-
],
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
Name: 'Education', DisplayName: 'Education',
|
|
177
|
-
Description: 'Education / degree records for Persons.',
|
|
178
|
-
SupportsWrite: true,
|
|
179
|
-
Fields: [
|
|
180
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Education record ID' },
|
|
181
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
182
|
-
],
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
Name: 'Payments', DisplayName: 'Payment',
|
|
186
|
-
Description: 'Payment transactions against Orders / Invoices.',
|
|
187
|
-
SupportsWrite: true,
|
|
188
|
-
Fields: [
|
|
189
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Payment record ID' },
|
|
190
|
-
{ Name: 'OrderId', DisplayName: 'Order ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Orders' },
|
|
191
|
-
],
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
Name: 'Invoices', DisplayName: 'Invoice',
|
|
195
|
-
Description: 'Invoice headers issued for Orders.',
|
|
196
|
-
SupportsWrite: true,
|
|
197
|
-
Fields: [
|
|
198
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Invoice record ID' },
|
|
199
|
-
{ Name: 'OrderId', DisplayName: 'Order ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Orders' },
|
|
200
|
-
],
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
Name: 'Chapters', DisplayName: 'Chapter',
|
|
204
|
-
Description: 'Geographic / topical chapters or branches.',
|
|
205
|
-
SupportsWrite: true,
|
|
206
|
-
Fields: [
|
|
207
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Chapter record ID' },
|
|
208
|
-
],
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
Name: 'Subscriptions', DisplayName: 'Subscription',
|
|
212
|
-
Description: 'Publication / content subscription records.',
|
|
213
|
-
SupportsWrite: true,
|
|
214
|
-
Fields: [
|
|
215
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Subscription record ID' },
|
|
216
|
-
{ Name: 'PersonId', DisplayName: 'Person ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'FK → Persons' },
|
|
217
|
-
],
|
|
218
|
-
},
|
|
219
|
-
];
|
|
220
|
-
const WATERMARK_CANDIDATE_FIELDS = ['LastModifiedDate', 'ModifiedDate', 'DateModified', 'UpdatedOn'];
|
|
221
|
-
// ─── Connector ──────────────────────────────────────────────────────────
|
|
222
|
-
let AptifyConnector = class AptifyConnector extends BaseRESTIntegrationConnector {
|
|
223
|
-
constructor() {
|
|
224
|
-
super(...arguments);
|
|
225
|
-
this.authState = null;
|
|
226
|
-
this.lastRequestTime = 0;
|
|
227
|
-
}
|
|
228
|
-
// ── Capability flags ────────────────────────────────────────────
|
|
229
|
-
get IntegrationName() { return 'Aptify'; }
|
|
230
|
-
get SupportsCreate() { return true; }
|
|
231
|
-
get SupportsUpdate() { return true; }
|
|
232
|
-
get SupportsDelete() { return true; }
|
|
233
|
-
get SupportsSearch() { return true; }
|
|
234
|
-
get SupportsListing() { return true; }
|
|
235
|
-
GetIntegrationObjects() {
|
|
236
|
-
return APT_OBJECTS;
|
|
237
|
-
}
|
|
238
|
-
GetActionGeneratorConfig() {
|
|
239
|
-
const config = super.GetActionGeneratorConfig();
|
|
240
|
-
if (!config)
|
|
241
|
-
return null;
|
|
242
|
-
config.IconClass = 'fa-solid fa-building-columns';
|
|
243
|
-
config.CategoryDescription = 'Aptify association management — persons, memberships, events, orders, and committees.';
|
|
244
|
-
config.ParentCategoryName = 'Association Management';
|
|
245
|
-
config.IncludeSearch = true;
|
|
246
|
-
config.IncludeList = true;
|
|
247
|
-
return config;
|
|
248
|
-
}
|
|
249
|
-
// ── Auth + transport (abstract methods from BaseRESTIntegrationConnector) ──
|
|
250
|
-
async Authenticate(companyIntegration, contextUser) {
|
|
251
|
-
return this.GetAuth(companyIntegration, contextUser);
|
|
252
|
-
}
|
|
253
|
-
BuildHeaders(auth) {
|
|
254
|
-
return {
|
|
255
|
-
'Authorization': `Web ${auth.Token}`,
|
|
256
|
-
'Accept': 'application/json',
|
|
257
|
-
'User-Agent': 'MemberJunction-Integration/1.0',
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
261
|
-
const aptifyAuth = auth;
|
|
262
|
-
const config = aptifyAuth.Config;
|
|
263
|
-
const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
264
|
-
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
265
|
-
const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
266
|
-
const effectiveHeaders = { ...headers };
|
|
267
|
-
if (body !== undefined && method !== 'GET' && method !== 'DELETE' && !effectiveHeaders['Content-Type']) {
|
|
268
|
-
effectiveHeaders['Content-Type'] = 'application/json';
|
|
269
|
-
}
|
|
270
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
271
|
-
await this.Throttle(minInterval);
|
|
272
|
-
const response = await this.ExecuteFetch(url, method, effectiveHeaders, body, timeoutMs);
|
|
273
|
-
this.lastRequestTime = Date.now();
|
|
274
|
-
if (response.status === 401 && attempt < maxRetries) {
|
|
275
|
-
this.authState = null;
|
|
276
|
-
const fresh = await this.GetAuth(aptifyAuth.Config);
|
|
277
|
-
aptifyAuth.Token = fresh.Token;
|
|
278
|
-
aptifyAuth.ExpiresAt = fresh.ExpiresAt;
|
|
279
|
-
effectiveHeaders['Authorization'] = `Web ${fresh.Token}`;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (this.ShouldRetry(response.status) && attempt < maxRetries) {
|
|
283
|
-
await this.Sleep(this.ComputeRetryDelay(response, attempt));
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
return this.BuildRESTResponse(response);
|
|
287
|
-
}
|
|
288
|
-
throw new Error(`Aptify API request failed after ${maxRetries} attempt(s): ${url}`);
|
|
289
|
-
}
|
|
290
|
-
NormalizeResponse(rawBody, responseDataKey) {
|
|
291
|
-
if (rawBody == null)
|
|
292
|
-
return [];
|
|
293
|
-
if (Array.isArray(rawBody))
|
|
294
|
-
return rawBody;
|
|
295
|
-
const body = rawBody;
|
|
296
|
-
if (responseDataKey && Array.isArray(body[responseDataKey])) {
|
|
297
|
-
return body[responseDataKey];
|
|
298
|
-
}
|
|
299
|
-
if (Array.isArray(body['Items']))
|
|
300
|
-
return body['Items'];
|
|
301
|
-
if (Array.isArray(body['value']))
|
|
302
|
-
return body['value'];
|
|
303
|
-
if (Object.keys(body).length > 0)
|
|
304
|
-
return [body];
|
|
305
|
-
return [];
|
|
306
|
-
}
|
|
307
|
-
ExtractPaginationInfo(rawBody, paginationType, currentPage, currentOffset, pageSize) {
|
|
308
|
-
if (paginationType === 'None')
|
|
309
|
-
return { HasMore: false };
|
|
310
|
-
const records = this.NormalizeResponse(rawBody, null);
|
|
311
|
-
const effectivePageSize = pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE;
|
|
312
|
-
const hasMore = records.length >= effectivePageSize;
|
|
313
|
-
switch (paginationType) {
|
|
314
|
-
case 'Offset':
|
|
315
|
-
return { HasMore: hasMore, NextOffset: currentOffset + records.length };
|
|
316
|
-
case 'PageNumber':
|
|
317
|
-
return { HasMore: hasMore, NextPage: currentPage + 1 };
|
|
318
|
-
default:
|
|
319
|
-
return { HasMore: hasMore };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
GetBaseURL(_companyIntegration, auth) {
|
|
323
|
-
return auth.BaseURL;
|
|
324
|
-
}
|
|
325
|
-
/** Aptify list endpoints use OData `$top` / `$skip` (capitalized property names). */
|
|
326
|
-
BuildPaginatedURL(basePath, obj, page, offset, _cursor, effectivePageSize) {
|
|
327
|
-
const pageSize = effectivePageSize ?? obj.DefaultPageSize ?? DEFAULT_PAGE_SIZE;
|
|
328
|
-
const sep = basePath.includes('?') ? '&' : '?';
|
|
329
|
-
switch (obj.PaginationType) {
|
|
330
|
-
case 'Offset':
|
|
331
|
-
return `${basePath}${sep}$top=${pageSize}&$skip=${offset}`;
|
|
332
|
-
case 'PageNumber':
|
|
333
|
-
return `${basePath}${sep}$top=${pageSize}&$skip=${(page - 1) * pageSize}`;
|
|
334
|
-
default:
|
|
335
|
-
return basePath;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// ── Discovery + connection test ─────────────────────────────────
|
|
339
|
-
async TestConnection(companyIntegration, contextUser) {
|
|
340
|
-
try {
|
|
341
|
-
const auth = await this.GetAuth(companyIntegration, contextUser, true);
|
|
342
|
-
const url = `${auth.BaseURL}/services/Persons?$top=1`;
|
|
343
|
-
const headers = this.BuildHeaders(auth);
|
|
344
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
345
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
346
|
-
return { Success: true, Message: `Connected to Aptify at ${auth.BaseURL}` };
|
|
347
|
-
}
|
|
348
|
-
return { Success: false, Message: `Aptify responded HTTP ${response.Status}` };
|
|
349
|
-
}
|
|
350
|
-
catch (err) {
|
|
351
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
352
|
-
return { Success: false, Message: `Aptify connection failed: ${message}` };
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
async DiscoverObjects(companyIntegration, contextUser) {
|
|
356
|
-
const staticObjects = await super.DiscoverObjects(companyIntegration, contextUser);
|
|
357
|
-
if (staticObjects.length > 0)
|
|
358
|
-
return staticObjects;
|
|
359
|
-
return APT_OBJECTS.map(o => ({
|
|
360
|
-
Name: o.Name,
|
|
361
|
-
Label: o.DisplayName,
|
|
362
|
-
Description: o.Description,
|
|
363
|
-
SupportsIncrementalSync: true,
|
|
364
|
-
SupportsWrite: o.SupportsWrite,
|
|
365
|
-
}));
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Discovery strategy: fetch one sample record from `${BaseURL}/services/{Object}?$top=1`
|
|
369
|
-
* and infer fields from its JSON shape, overlaying static PK / FK metadata where we have it.
|
|
370
|
-
* Falls back to the static field set if the live call fails (offline scenarios, sandboxes
|
|
371
|
-
* with empty tables, etc.).
|
|
372
|
-
*/
|
|
373
|
-
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
374
|
-
try {
|
|
375
|
-
const auth = await this.GetAuth(companyIntegration, contextUser);
|
|
376
|
-
const url = `${auth.BaseURL}/services/${encodeURIComponent(objectName)}?$top=1`;
|
|
377
|
-
const headers = this.BuildHeaders(auth);
|
|
378
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
379
|
-
if (response.Status === 200) {
|
|
380
|
-
const records = this.NormalizeResponse(response.Body, null);
|
|
381
|
-
if (records.length > 0)
|
|
382
|
-
return this.InferFieldsFromSample(records[0], objectName);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch {
|
|
386
|
-
// Live discovery failed — fall through to the static fallback below.
|
|
387
|
-
}
|
|
388
|
-
const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
|
|
389
|
-
if (staticFields.length > 0)
|
|
390
|
-
return staticFields;
|
|
391
|
-
const obj = APT_OBJECTS.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
|
|
392
|
-
if (!obj)
|
|
393
|
-
return [];
|
|
394
|
-
return obj.Fields.map(f => {
|
|
395
|
-
const fkTarget = this.ResolveForeignKeyTarget(f.Description);
|
|
396
|
-
return {
|
|
397
|
-
Name: f.Name,
|
|
398
|
-
Label: f.DisplayName,
|
|
399
|
-
Description: f.Description,
|
|
400
|
-
DataType: f.Type,
|
|
401
|
-
IsRequired: f.IsRequired,
|
|
402
|
-
IsUniqueKey: f.IsPrimaryKey,
|
|
403
|
-
IsReadOnly: f.IsReadOnly,
|
|
404
|
-
IsForeignKey: fkTarget != null,
|
|
405
|
-
ForeignKeyTarget: fkTarget,
|
|
406
|
-
};
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
/** Pull a sample record's keys + values, overlay any static PK/FK metadata we have for the object. */
|
|
410
|
-
InferFieldsFromSample(sample, objectName) {
|
|
411
|
-
const staticObj = APT_OBJECTS.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
|
|
412
|
-
const staticMap = new Map((staticObj?.Fields ?? []).map(f => [f.Name.toLowerCase(), f]));
|
|
413
|
-
return Object.entries(sample).map(([key, value]) => {
|
|
414
|
-
const overlay = staticMap.get(key.toLowerCase());
|
|
415
|
-
const fkTarget = this.ResolveForeignKeyTarget(overlay?.Description);
|
|
416
|
-
return {
|
|
417
|
-
Name: key,
|
|
418
|
-
Label: overlay?.DisplayName ?? key,
|
|
419
|
-
Description: overlay?.Description ?? '',
|
|
420
|
-
DataType: overlay?.Type ?? this.InferType(value),
|
|
421
|
-
IsRequired: overlay?.IsRequired ?? false,
|
|
422
|
-
IsUniqueKey: overlay?.IsPrimaryKey ?? false,
|
|
423
|
-
IsReadOnly: overlay?.IsReadOnly ?? false,
|
|
424
|
-
IsForeignKey: fkTarget != null,
|
|
425
|
-
ForeignKeyTarget: fkTarget,
|
|
426
|
-
};
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Promotes a foreign-key target that this connector already declares in a field's
|
|
431
|
-
* `Description` (the `FK → <Object>` convention used throughout APT_OBJECTS) into the
|
|
432
|
-
* proper `ForeignKeyTarget` slot. Only resolves when the named target is itself a
|
|
433
|
-
* sibling object declared in APT_OBJECTS — otherwise returns null (no guessing).
|
|
434
|
-
*/
|
|
435
|
-
ResolveForeignKeyTarget(description) {
|
|
436
|
-
if (!description || !description.startsWith('FK'))
|
|
437
|
-
return null;
|
|
438
|
-
const match = description.match(/FK\s*→\s*(\S+)/);
|
|
439
|
-
const target = match?.[1];
|
|
440
|
-
if (!target)
|
|
441
|
-
return null;
|
|
442
|
-
const sibling = APT_OBJECTS.find(o => o.Name.toLowerCase() === target.toLowerCase());
|
|
443
|
-
return sibling ? sibling.Name : null;
|
|
444
|
-
}
|
|
445
|
-
InferType(value) {
|
|
446
|
-
if (value === null || value === undefined)
|
|
447
|
-
return 'string';
|
|
448
|
-
if (typeof value === 'number')
|
|
449
|
-
return Number.isInteger(value) ? 'integer' : 'decimal';
|
|
450
|
-
if (typeof value === 'boolean')
|
|
451
|
-
return 'boolean';
|
|
452
|
-
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value))
|
|
453
|
-
return 'datetime';
|
|
454
|
-
return 'string';
|
|
455
|
-
}
|
|
456
|
-
// ── Fetch + watermark ───────────────────────────────────────────
|
|
457
|
-
async FetchChanges(ctx) {
|
|
458
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
459
|
-
const headers = this.BuildHeaders(auth);
|
|
460
|
-
const offset = ctx.CurrentOffset ?? 0;
|
|
461
|
-
const url = this.BuildFetchURL(auth.BaseURL, ctx.ObjectName, offset, ctx.WatermarkValue);
|
|
462
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
463
|
-
if (response.Status < 200 || response.Status >= 300) {
|
|
464
|
-
throw new Error(`Aptify FetchChanges failed for ${ctx.ObjectName}: HTTP ${response.Status}`);
|
|
465
|
-
}
|
|
466
|
-
const records = this.NormalizeResponse(response.Body, null);
|
|
467
|
-
const externalRecords = records.map(r => ({
|
|
468
|
-
ExternalID: this.ExtractIdFromRecord(r),
|
|
469
|
-
ObjectType: ctx.ObjectName,
|
|
470
|
-
Fields: r,
|
|
471
|
-
}));
|
|
472
|
-
return {
|
|
473
|
-
Records: externalRecords,
|
|
474
|
-
HasMore: records.length >= DEFAULT_PAGE_SIZE,
|
|
475
|
-
NextOffset: offset + records.length,
|
|
476
|
-
NewWatermarkValue: this.ComputeNewWatermark(externalRecords, ctx.WatermarkValue),
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
BuildFetchURL(baseURL, objectName, offset, watermark) {
|
|
480
|
-
const path = `${baseURL}/services/${encodeURIComponent(objectName)}`;
|
|
481
|
-
const params = new URLSearchParams();
|
|
482
|
-
params.set('$top', String(DEFAULT_PAGE_SIZE));
|
|
483
|
-
params.set('$skip', String(offset));
|
|
484
|
-
if (watermark) {
|
|
485
|
-
params.set('$filter', `LastModifiedDate ge datetime'${watermark}'`);
|
|
486
|
-
}
|
|
487
|
-
return `${path}?${params.toString()}`;
|
|
488
|
-
}
|
|
489
|
-
ComputeNewWatermark(records, current) {
|
|
490
|
-
let latest;
|
|
491
|
-
for (const record of records) {
|
|
492
|
-
for (const key of WATERMARK_CANDIDATE_FIELDS) {
|
|
493
|
-
const value = record.Fields[key];
|
|
494
|
-
if (typeof value === 'string' && (!latest || value > latest))
|
|
495
|
-
latest = value;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (!latest)
|
|
499
|
-
return current ?? undefined;
|
|
500
|
-
if (current && latest <= current)
|
|
501
|
-
return current;
|
|
502
|
-
return latest;
|
|
503
|
-
}
|
|
504
|
-
// ── CRUD ────────────────────────────────────────────────────────
|
|
505
|
-
async CreateRecord(ctx) {
|
|
506
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
507
|
-
const url = `${auth.BaseURL}/services/${encodeURIComponent(ctx.ObjectName)}`;
|
|
508
|
-
const response = await this.MakeHTTPRequest(auth, url, 'POST', this.BuildHeaders(auth), ctx.Attributes);
|
|
509
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
510
|
-
const body = (response.Body ?? {});
|
|
511
|
-
return this.BuildCreatedResult(this.ExtractIdFromRecord(body), response.Status, ctx.ObjectName);
|
|
512
|
-
}
|
|
513
|
-
return this.BuildCRUDError(response, 'CreateRecord', ctx.ObjectName);
|
|
514
|
-
}
|
|
515
|
-
async UpdateRecord(ctx) {
|
|
516
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
517
|
-
const url = `${auth.BaseURL}/services/${encodeURIComponent(ctx.ObjectName)}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
518
|
-
const response = await this.MakeHTTPRequest(auth, url, 'PUT', this.BuildHeaders(auth), ctx.Attributes);
|
|
519
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
520
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
521
|
-
}
|
|
522
|
-
return this.BuildCRUDError(response, 'UpdateRecord', ctx.ObjectName);
|
|
523
|
-
}
|
|
524
|
-
async DeleteRecord(ctx) {
|
|
525
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
526
|
-
const url = `${auth.BaseURL}/services/${encodeURIComponent(ctx.ObjectName)}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
527
|
-
const response = await this.MakeHTTPRequest(auth, url, 'DELETE', this.BuildHeaders(auth));
|
|
528
|
-
if (response.Status === 204 || (response.Status >= 200 && response.Status < 300)) {
|
|
529
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
530
|
-
}
|
|
531
|
-
return this.BuildCRUDError(response, 'DeleteRecord', ctx.ObjectName);
|
|
532
|
-
}
|
|
533
|
-
async GetRecord(ctx) {
|
|
534
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
535
|
-
const url = `${auth.BaseURL}/services/${encodeURIComponent(ctx.ObjectName)}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
536
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
537
|
-
if (response.Status < 200 || response.Status >= 300)
|
|
538
|
-
return null;
|
|
539
|
-
if (response.Body == null || typeof response.Body !== 'object')
|
|
540
|
-
return null;
|
|
541
|
-
const body = response.Body;
|
|
542
|
-
return { ExternalID: ctx.ExternalID, ObjectType: ctx.ObjectName, Fields: body };
|
|
543
|
-
}
|
|
544
|
-
// ── Default config + field mappings ─────────────────────────────
|
|
545
|
-
GetDefaultConfiguration() {
|
|
546
|
-
return { DefaultSchemaName: 'Aptify', DefaultObjects: [] };
|
|
547
|
-
}
|
|
548
|
-
GetDefaultFieldMappings(objectName, _entityName) {
|
|
549
|
-
const obj = APT_OBJECTS.find(o => o.Name.toLowerCase() === objectName.toLowerCase());
|
|
550
|
-
if (!obj)
|
|
551
|
-
return [];
|
|
552
|
-
return obj.Fields.map(f => ({
|
|
553
|
-
SourceFieldName: f.Name,
|
|
554
|
-
DestinationFieldName: f.Name === 'Id' ? 'ExternalID' : f.Name,
|
|
555
|
-
IsKeyField: f.IsPrimaryKey,
|
|
556
|
-
}));
|
|
557
|
-
}
|
|
558
|
-
// ─────────────────────────────────────────────────────────────────
|
|
559
|
-
// Private helpers
|
|
560
|
-
// ─────────────────────────────────────────────────────────────────
|
|
561
|
-
/**
|
|
562
|
-
* Returns a valid auth context, refreshing the cached token as needed. Accepts either
|
|
563
|
-
* (companyIntegration, contextUser) for normal callers or a pre-parsed config for the
|
|
564
|
-
* silent re-auth path inside MakeHTTPRequest.
|
|
565
|
-
*/
|
|
566
|
-
async GetAuth(source, contextUser, forceRefresh = false) {
|
|
567
|
-
if (!forceRefresh && this.authState && this.IsTokenValid(this.authState)) {
|
|
568
|
-
return this.authState;
|
|
569
|
-
}
|
|
570
|
-
const config = this.IsConfig(source)
|
|
571
|
-
? source
|
|
572
|
-
: await this.ParseConfig(source, contextUser);
|
|
573
|
-
this.authState = await this.AcquireToken(config);
|
|
574
|
-
return this.authState;
|
|
575
|
-
}
|
|
576
|
-
IsConfig(source) {
|
|
577
|
-
return source.BaseURL !== undefined
|
|
578
|
-
&& source.Username !== undefined;
|
|
579
|
-
}
|
|
580
|
-
IsTokenValid(auth) {
|
|
581
|
-
return auth.ExpiresAt.getTime() > Date.now() + TOKEN_REFRESH_BUFFER_MS;
|
|
582
|
-
}
|
|
583
|
-
async AcquireToken(config) {
|
|
584
|
-
const baseURL = this.StripTrailingSlash(config.BaseURL);
|
|
585
|
-
// Vendor canonical: POST {BaseURL}/Services/Authentication/Login/{Method}
|
|
586
|
-
// where {Method} is 'Web' (the AuthProvider name is encoded in the URL path,
|
|
587
|
-
// NOT a body field). Default to 'Web' (Aptify's standard internal provider) —
|
|
588
|
-
// override via AuthProvider for SSO/SAML providers.
|
|
589
|
-
const authMethod = config.AuthProvider && config.AuthProvider.trim().length > 0
|
|
590
|
-
? config.AuthProvider.trim()
|
|
591
|
-
: 'Web';
|
|
592
|
-
const tokenURL = `${baseURL}/Services/Authentication/Login/${encodeURIComponent(authMethod)}`;
|
|
593
|
-
// Aptify expects 'UserName' (capital N) per the vendor's Web API docs —
|
|
594
|
-
// 'Username' (lower n) would yield a 400/401.
|
|
595
|
-
const requestBody = {
|
|
596
|
-
UserName: config.Username,
|
|
597
|
-
Password: config.Password,
|
|
598
|
-
};
|
|
599
|
-
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
600
|
-
const response = await fetch(tokenURL, {
|
|
601
|
-
method: 'POST',
|
|
602
|
-
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
603
|
-
body: JSON.stringify(requestBody),
|
|
604
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
605
|
-
});
|
|
606
|
-
if (!response.ok) {
|
|
607
|
-
const detail = await response.text().catch(() => '');
|
|
608
|
-
throw new Error(`Aptify auth failed (${response.status}): ${detail.slice(0, 300)}`);
|
|
609
|
-
}
|
|
610
|
-
const payload = await response.json();
|
|
611
|
-
if (!payload?.TokenId) {
|
|
612
|
-
throw new Error('Aptify auth response missing TokenId');
|
|
613
|
-
}
|
|
614
|
-
return {
|
|
615
|
-
Token: payload.TokenId,
|
|
616
|
-
TokenType: 'Web',
|
|
617
|
-
ExpiresAt: new Date(Date.now() + TOKEN_LIFETIME_MS),
|
|
618
|
-
Config: { ...config, BaseURL: baseURL },
|
|
619
|
-
BaseURL: baseURL,
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
async ParseConfig(companyIntegration, contextUser) {
|
|
623
|
-
if (companyIntegration.CredentialID) {
|
|
624
|
-
const fromCred = await this.ParseConfigFromCredential(companyIntegration.CredentialID, contextUser);
|
|
625
|
-
if (fromCred)
|
|
626
|
-
return fromCred;
|
|
627
|
-
}
|
|
628
|
-
if (companyIntegration.Configuration) {
|
|
629
|
-
const parsed = JSON.parse(companyIntegration.Configuration);
|
|
630
|
-
return this.ExtractConfig(parsed);
|
|
631
|
-
}
|
|
632
|
-
throw new Error('Aptify connector requires either CredentialID or Configuration JSON');
|
|
633
|
-
}
|
|
634
|
-
async ParseConfigFromCredential(credentialID, contextUser, provider) {
|
|
635
|
-
const md = provider ?? new Metadata();
|
|
636
|
-
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
637
|
-
const loaded = await credential.Load(credentialID);
|
|
638
|
-
if (!loaded || !credential.Values)
|
|
639
|
-
return null;
|
|
640
|
-
const values = JSON.parse(credential.Values);
|
|
641
|
-
return this.ExtractConfig(values);
|
|
642
|
-
}
|
|
643
|
-
ExtractConfig(values) {
|
|
644
|
-
const get = (...keys) => {
|
|
645
|
-
for (const key of keys) {
|
|
646
|
-
const hit = Object.entries(values).find(([k]) => k.toLowerCase() === key.toLowerCase());
|
|
647
|
-
if (hit)
|
|
648
|
-
return String(hit[1] ?? '');
|
|
649
|
-
}
|
|
650
|
-
return undefined;
|
|
651
|
-
};
|
|
652
|
-
const baseURL = get('BaseURL', 'BaseUrl', 'base_url');
|
|
653
|
-
const username = get('Username', 'username', 'user');
|
|
654
|
-
const password = get('Password', 'password', 'pass');
|
|
655
|
-
if (!baseURL)
|
|
656
|
-
throw new Error('Aptify configuration missing required field: BaseURL');
|
|
657
|
-
if (!username)
|
|
658
|
-
throw new Error('Aptify configuration missing required field: Username');
|
|
659
|
-
if (!password)
|
|
660
|
-
throw new Error('Aptify configuration missing required field: Password');
|
|
661
|
-
return {
|
|
662
|
-
BaseURL: this.StripTrailingSlash(baseURL),
|
|
663
|
-
Username: username,
|
|
664
|
-
Password: password,
|
|
665
|
-
AuthProvider: get('AuthProvider', 'authProvider', 'auth_provider'),
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
// ── HTTP helpers ────────────────────────────────────────────────
|
|
669
|
-
async ExecuteFetch(url, method, headers, body, timeoutMs) {
|
|
670
|
-
const controller = new AbortController();
|
|
671
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
672
|
-
try {
|
|
673
|
-
const requestInit = { method, headers, signal: controller.signal };
|
|
674
|
-
if (body !== undefined && method !== 'GET' && method !== 'DELETE') {
|
|
675
|
-
requestInit.body = JSON.stringify(body);
|
|
676
|
-
}
|
|
677
|
-
return await fetch(url, requestInit);
|
|
678
|
-
}
|
|
679
|
-
finally {
|
|
680
|
-
clearTimeout(timer);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
async BuildRESTResponse(response) {
|
|
684
|
-
const headers = {};
|
|
685
|
-
response.headers.forEach((v, k) => { headers[k.toLowerCase()] = v; });
|
|
686
|
-
const contentType = headers['content-type'] ?? '';
|
|
687
|
-
const body = contentType.includes('application/json')
|
|
688
|
-
? await this.ParseJsonSafely(response)
|
|
689
|
-
: await response.text();
|
|
690
|
-
return { Status: response.status, Body: body, Headers: headers };
|
|
691
|
-
}
|
|
692
|
-
async ParseJsonSafely(response) {
|
|
693
|
-
try {
|
|
694
|
-
return await response.json();
|
|
695
|
-
}
|
|
696
|
-
catch {
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
ShouldRetry(status) {
|
|
701
|
-
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
702
|
-
}
|
|
703
|
-
ComputeRetryDelay(response, attempt) {
|
|
704
|
-
const retryAfter = response.headers.get('retry-after');
|
|
705
|
-
if (retryAfter) {
|
|
706
|
-
const seconds = Number.parseInt(retryAfter, 10);
|
|
707
|
-
if (!Number.isNaN(seconds) && seconds > 0)
|
|
708
|
-
return seconds * 1000;
|
|
709
|
-
}
|
|
710
|
-
return Math.min(Math.pow(2, attempt) * 1000, 15_000);
|
|
711
|
-
}
|
|
712
|
-
async Throttle(minIntervalMs) {
|
|
713
|
-
const elapsed = Date.now() - this.lastRequestTime;
|
|
714
|
-
if (elapsed < minIntervalMs)
|
|
715
|
-
await this.Sleep(minIntervalMs - elapsed);
|
|
716
|
-
}
|
|
717
|
-
Sleep(ms) {
|
|
718
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
719
|
-
}
|
|
720
|
-
// ── Misc helpers ────────────────────────────────────────────────
|
|
721
|
-
ExtractIdFromRecord(body) {
|
|
722
|
-
const candidates = ['Id', 'ID', 'id'];
|
|
723
|
-
for (const key of candidates) {
|
|
724
|
-
const value = body[key];
|
|
725
|
-
if (value !== undefined && value !== null)
|
|
726
|
-
return String(value);
|
|
727
|
-
}
|
|
728
|
-
return '';
|
|
729
|
-
}
|
|
730
|
-
StripTrailingSlash(url) {
|
|
731
|
-
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
732
|
-
}
|
|
733
|
-
BuildCRUDError(response, operation, objectName) {
|
|
734
|
-
const body = response.Body;
|
|
735
|
-
let detail;
|
|
736
|
-
if (typeof body === 'string') {
|
|
737
|
-
detail = body.slice(0, 500);
|
|
738
|
-
}
|
|
739
|
-
else if (body && typeof body === 'object') {
|
|
740
|
-
const asRecord = body;
|
|
741
|
-
const message = asRecord['Message'] ?? asRecord['error'] ?? asRecord['error_description'];
|
|
742
|
-
detail = message !== undefined ? String(message) : JSON.stringify(body).slice(0, 500);
|
|
743
|
-
}
|
|
744
|
-
else {
|
|
745
|
-
detail = `HTTP ${response.Status}`;
|
|
746
|
-
}
|
|
747
|
-
return {
|
|
748
|
-
Success: false,
|
|
749
|
-
ErrorMessage: `Aptify ${operation} failed on ${objectName}: ${detail}`,
|
|
750
|
-
StatusCode: response.Status,
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
AptifyConnector = __decorate([
|
|
755
|
-
RegisterClass(BaseIntegrationConnector, 'AptifyConnector')
|
|
756
|
-
], AptifyConnector);
|
|
757
|
-
export { AptifyConnector };
|
|
758
|
-
/** Tree-shaking prevention — import and call from module entry point. */
|
|
759
|
-
export function LoadAptifyConnector() { }
|
|
760
|
-
//# sourceMappingURL=AptifyConnector.js.map
|