@memberjunction/connector-netsuite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/NetSuiteConnector.d.ts +135 -0
- package/dist/NetSuiteConnector.js +703 -0
- package/dist/NetSuiteConnector.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type UserInfo } from '@memberjunction/core';
|
|
2
|
+
import type { MJCompanyIntegrationEntity } from '@memberjunction/core-entities';
|
|
3
|
+
import { BaseRESTIntegrationConnector, type RESTAuthContext, type RESTResponse, type PaginationState, type PaginationType, type ConnectionTestResult, type FetchContext, type FetchBatchResult, type ExternalObjectSchema, type ExternalFieldSchema, type RateLimitPolicy } from '@memberjunction/integration-engine';
|
|
4
|
+
export declare class NetSuiteConnector extends BaseRESTIntegrationConnector {
|
|
5
|
+
private oauth2Manager;
|
|
6
|
+
get IntegrationName(): string;
|
|
7
|
+
get SupportsCreate(): boolean;
|
|
8
|
+
get SupportsUpdate(): boolean;
|
|
9
|
+
get SupportsDelete(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* NON-authoritative for deactivation (deliberately false — the T3-deadlock-safe posture).
|
|
12
|
+
*
|
|
13
|
+
* The metadata-catalog enumerates the Record-Service record types (standard + custom) — but it is
|
|
14
|
+
* NOT the complete gamut of this connector's emitted IOs: the contract retains sublist-access-path
|
|
15
|
+
* children (InvoiceItem, SalesOrderItem) that are reachable as nested graph children and NEVER
|
|
16
|
+
* appear in the record-type catalog (SublistScopeExtension). It also returns lowercase record-type
|
|
17
|
+
* SLUGS (`phonecall`) while Declared standard objects are humanized (`Phone Call`). An authoritative
|
|
18
|
+
* (deactivating) refresh would therefore wrongly Disable every Declared sublist child and any object
|
|
19
|
+
* whose catalog slug doesn't byte-match the Declared name. Per the convention rule — a partial/scoped
|
|
20
|
+
* enumeration is NEVER authoritative — absence here proves nothing, so we deactivate nothing. The
|
|
21
|
+
* standard universe is the Declared metadata baseline; a credential only ADDS the account's customs.
|
|
22
|
+
*/
|
|
23
|
+
get DiscoveryIsAuthoritative(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* NetSuite governs by CONCURRENT-request limit (5–50 by tier), not requests/sec. We cap inner
|
|
26
|
+
* concurrency conservatively below the smallest tier; the engine's adaptive controller ramps within it.
|
|
27
|
+
*/
|
|
28
|
+
get MaxConcurrencyHint(): number;
|
|
29
|
+
/** No documented RPS policy — concurrency-governed. Return null so the engine derives a conservative rate. */
|
|
30
|
+
get RateLimitPolicy(): RateLimitPolicy | null;
|
|
31
|
+
/**
|
|
32
|
+
* SuiteQL fetches in `ORDER BY lastModifiedDate` and an updated record always re-surfaces at a new,
|
|
33
|
+
* higher modstamp — so the last batch's max IS the true high-water mark. Safe for the engine to use
|
|
34
|
+
* the returned watermark to narrow the next incremental.
|
|
35
|
+
*/
|
|
36
|
+
get MonotonicWatermark(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* For watermark-less objects, NetSuite's universal numeric `id` is a stable monotonic ordering key
|
|
39
|
+
* usable for keyset/seek resume.
|
|
40
|
+
*/
|
|
41
|
+
StableOrderingKey(_objectName: string): string | null;
|
|
42
|
+
/** Parse NetSuite's 429 Retry-After (seconds) into ms; concurrency limit errors carry no precise hint. */
|
|
43
|
+
ExtractRetryAfterMs(error: unknown): number | undefined;
|
|
44
|
+
private ParseConfig;
|
|
45
|
+
private ValidateConfig;
|
|
46
|
+
/** Determines the auth mode from explicit AuthFlow or the credential keys present. */
|
|
47
|
+
private ResolveAuthMode;
|
|
48
|
+
protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<RESTAuthContext>;
|
|
49
|
+
private AssertTBACredentials;
|
|
50
|
+
/**
|
|
51
|
+
* Resolves a bearer token for OAuth2 mode. A pre-minted BearerToken/AccessToken is used directly
|
|
52
|
+
* (NetSuite OAuth2 access tokens are supplied by the operator). When a RefreshToken + TokenURL +
|
|
53
|
+
* client credentials are present, the shared OAuth2TokenManager refreshes — crypto-free token round-trip.
|
|
54
|
+
*/
|
|
55
|
+
private ResolveBearerToken;
|
|
56
|
+
/**
|
|
57
|
+
* Base headers WITHOUT an Authorization value — the auth header is per-request (OAuth 1.0a signs
|
|
58
|
+
* each URL+method) so it's injected in MakeHTTPRequest, not here. The base class calls BuildHeaders
|
|
59
|
+
* for its generic CRUD / pagination GETs; MakeHTTPRequest stamps the signed/bearer Authorization.
|
|
60
|
+
*/
|
|
61
|
+
protected BuildHeaders(_auth: RESTAuthContext): Record<string, string>;
|
|
62
|
+
/** Builds the Authorization header for a specific URL + method (TBA signs per request; OAuth2 is static). */
|
|
63
|
+
private BuildAuthorizationHeader;
|
|
64
|
+
/** Base URL = host root; the IO's APIPath already carries the full /services/rest/... path. */
|
|
65
|
+
protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: RESTAuthContext): string;
|
|
66
|
+
protected MakeHTTPRequest(auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
|
|
67
|
+
private BuildRESTResponse;
|
|
68
|
+
private ParseRetryAfterHeader;
|
|
69
|
+
private IsTransientNetworkError;
|
|
70
|
+
private BackoffDelay;
|
|
71
|
+
private Sleep;
|
|
72
|
+
/** Strips the NetSuite HATEOAS envelope: { items: [...] } → the items array; bare object → [object]. */
|
|
73
|
+
protected NormalizeResponse(rawBody: unknown, _responseDataKey: string | null): Record<string, unknown>[];
|
|
74
|
+
/** Offset pagination: hasMore flag + offset advance (or the next-link href when present). */
|
|
75
|
+
protected ExtractPaginationInfo(rawBody: unknown, _paginationType: PaginationType, _currentPage: number, currentOffset: number, pageSize: number): PaginationState;
|
|
76
|
+
/** Reads the `offset` query param from the HATEOAS rel='next' link when the envelope provides one. */
|
|
77
|
+
private NextOffsetFromLinks;
|
|
78
|
+
/** Validates credentials + host by running a trivial SuiteQL probe (no record dependency). */
|
|
79
|
+
TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Discovers objects by UNIONING the Declared baseline with the live metadata-catalog (case-2
|
|
82
|
+
* auth-gated discovery). The Declared metadata (200+ standard record types + the sublist-access-path
|
|
83
|
+
* children that the catalog never lists) is the floor — it is ALWAYS surfaced so a refresh can't drop
|
|
84
|
+
* it. The catalog's record-type SLUGS (`phonecall`) are name-aligned back to the Declared name via the
|
|
85
|
+
* cached Configuration.recordTypeSlug so a slug never duplicates an existing Declared object; an
|
|
86
|
+
* unknown slug passes through as a NEW custom record. Net: standard universe always present + tenant
|
|
87
|
+
* customs added. Non-authoritative (DiscoveryIsAuthoritative=false), so no object is ever deactivated.
|
|
88
|
+
*/
|
|
89
|
+
DiscoverObjects(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
|
|
90
|
+
/**
|
|
91
|
+
* The Declared (cached) baseline objects — the floor DiscoverObjects always surfaces. Seam over the
|
|
92
|
+
* base class's cache read so the union logic is unit-testable without a DB-backed engine.
|
|
93
|
+
*/
|
|
94
|
+
protected GetDeclaredObjects(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
|
|
95
|
+
/** Maps each Declared IO's Configuration.recordTypeSlug → its Declared Name (for catalog name-alignment). */
|
|
96
|
+
protected BuildSlugToDeclaredNameMap(integrationID: string): Map<string, string>;
|
|
97
|
+
/**
|
|
98
|
+
* Resolves the metadata-catalog slug for an object name: the cached IO's Configuration.recordTypeSlug
|
|
99
|
+
* when present (the authoritative slug, e.g. `phonecall` for `Phone Call`), else a name collapse.
|
|
100
|
+
*/
|
|
101
|
+
private ResolveRecordTypeSlug;
|
|
102
|
+
/** Tolerantly reads Configuration.recordTypeSlug from an IO's Configuration JSON. */
|
|
103
|
+
private ReadRecordTypeSlug;
|
|
104
|
+
/** Pulls record-type names from the catalog's `items[].name` or `links[].href` trailing segment. */
|
|
105
|
+
private ExtractCatalogNames;
|
|
106
|
+
/**
|
|
107
|
+
* Discovers fields for a record type from the metadata-catalog JSON-schema for that type. The schema
|
|
108
|
+
* `properties` map → fields; `required` → IsRequired; `readOnly`/computed markers → IsReadOnly; the
|
|
109
|
+
* universal numeric `id` is the PK. Falls back to the cached Declared fields when the catalog can't
|
|
110
|
+
* describe this type.
|
|
111
|
+
*/
|
|
112
|
+
DiscoverFields(companyIntegration: MJCompanyIntegrationEntity, objectName: string, contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
|
|
113
|
+
/** Maps a JSON-schema describe body to ExternalFieldSchema[] (provable-only: only stated flags). */
|
|
114
|
+
private ParseFieldSchema;
|
|
115
|
+
/** Maps a JSON-schema property to a generic source type. */
|
|
116
|
+
private MapSchemaType;
|
|
117
|
+
private HumanizeName;
|
|
118
|
+
FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
|
|
119
|
+
/** Builds the SuiteQL SELECT, applying the incremental watermark predicate when a watermark exists. */
|
|
120
|
+
private BuildSuiteQL;
|
|
121
|
+
/** Quotes a watermark value for SuiteQL — wraps a NetSuite datetime in TO_DATE/quotes, escaping quotes. */
|
|
122
|
+
private QuoteSuiteQLDate;
|
|
123
|
+
/** Returns the maximum value of the watermark field across the batch (string compare on ISO timestamps). */
|
|
124
|
+
private MaxWatermark;
|
|
125
|
+
/**
|
|
126
|
+
* Builds an ExternalRecord carrying the FULL source record in Fields (full-record pass-through, so the
|
|
127
|
+
* framework's custom-column capture sees every key) and the universal `id` as the ExternalID. Mirrors
|
|
128
|
+
* the base class's composite-PK + content-hash fallback at the connector boundary for hand-built records.
|
|
129
|
+
*/
|
|
130
|
+
private BuildExternalRecord;
|
|
131
|
+
/** Reads the IO's Configuration JSON (suiteQLTable, defaultPageSize) tolerantly. */
|
|
132
|
+
private ReadIOConfig;
|
|
133
|
+
private PreviewBody;
|
|
134
|
+
}
|
|
135
|
+
export declare function LoadNetSuiteConnector(): void;
|
|
@@ -0,0 +1,703 @@
|
|
|
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
|
+
* NetSuiteConnector — Integration connector for Oracle NetSuite (SuiteTalk REST Web Services).
|
|
9
|
+
*
|
|
10
|
+
* API docs: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/ (SuiteTalk REST API Guide)
|
|
11
|
+
*
|
|
12
|
+
* SCOPE: SuiteTalk REST Web Services only — the Record Service (CRUD on standard + custom record
|
|
13
|
+
* types) + the SuiteQL query service. SOAP / RESTlets / SuiteAnalytics Connect are out of scope.
|
|
14
|
+
*
|
|
15
|
+
* ── Auth (selected per-connection by Configuration / credential) ───────────────────────────────
|
|
16
|
+
* • OAuth 1.0a TBA (Token-Based Authentication) — the primary documented flow. Long-lived,
|
|
17
|
+
* non-expiring ConsumerKey/ConsumerSecret + TokenID/TokenSecret, signed per request with
|
|
18
|
+
* HMAC-SHA256 and a per-account `realm`. Signing is delegated to the shared `OAuth1aSigner`
|
|
19
|
+
* auth-helper — NEVER inline crypto.
|
|
20
|
+
* • OAuth 2.0 bearer — the documented alternate (Authorization Code flow). A pre-minted bearer
|
|
21
|
+
* access token is supplied via the credential/Configuration and sent as `Authorization: Bearer`.
|
|
22
|
+
* (Refresh, when a refresh token + token URL are present, is delegated to OAuth2TokenManager.)
|
|
23
|
+
* Mode is chosen by Configuration.AuthFlow ('oauth1-tba' | 'oauth2') or inferred from the
|
|
24
|
+
* credential keys present (BearerToken/AccessToken ⇒ OAuth2; ConsumerKey+TokenID ⇒ TBA).
|
|
25
|
+
*
|
|
26
|
+
* ── Host (tenant-agnostic — NO baked account) ──────────────────────────────────────────────────
|
|
27
|
+
* The base URL is built at runtime from Configuration.AccountID:
|
|
28
|
+
* `https://{accountId}.suitetalk.api.netsuite.com` (e.g. '1234567' prod, '1234567_SB1' sandbox).
|
|
29
|
+
* Per-account subdomains require the account id be lowercased and '_' → '-' in the subdomain.
|
|
30
|
+
*
|
|
31
|
+
* ── Reads ──────────────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
* FetchChanges runs SuiteQL: POST /services/rest/query/v1/suiteql with the SQL in the body
|
|
33
|
+
* `{ q: '<SQL>' }` and the REQUIRED header `Prefer: transient`. This expresses the lastModifiedDate
|
|
34
|
+
* incremental watermark predicate (a thing the generic GET path cannot), so FetchChanges is the one
|
|
35
|
+
* genuinely-idiosyncratic override. Everything else rides the base.
|
|
36
|
+
*
|
|
37
|
+
* ── CRUD ───────────────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
* Create/Update/Delete/Get use the base class's GENERIC per-operation path — they read the IO's
|
|
39
|
+
* Create/Update/Delete APIPath/Method/BodyShape/BodyKey/IDLocation columns and dispatch. NetSuite's
|
|
40
|
+
* Record Service CRUD is a plain REST shape (POST collection, PATCH /{id}, DELETE /{id}), so no
|
|
41
|
+
* override is needed or written.
|
|
42
|
+
*
|
|
43
|
+
* ── Pagination ─────────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
* Offset/limit (default 1000). Responses carry the HATEOAS envelope { items, count, totalResults,
|
|
45
|
+
* hasMore, offset, links:[{rel:'next',href}] } — surfaced in NormalizeResponse + ExtractPaginationInfo.
|
|
46
|
+
*
|
|
47
|
+
* ── Incremental ────────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
* lastModifiedDate watermark (via WatermarkService in the engine; the connector applies the IO's
|
|
49
|
+
* IncrementalWatermarkField inside the SuiteQL predicate and returns the max-seen on full-batch success).
|
|
50
|
+
*
|
|
51
|
+
* ── Discovery ──────────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
* Rides the metadata-catalog MECHANISM (no baked catalog): GET /services/rest/record/v1/metadata-catalog
|
|
53
|
+
* enumerates the record types accessible to the credential; per-type field schema is fetched from the
|
|
54
|
+
* same catalog (Accept: application/schema+json). DiscoverObjects UNIONS the Declared baseline (the 200+
|
|
55
|
+
* standard record types + the sublist-access-path children, which the catalog never lists) with the live
|
|
56
|
+
* catalog's record types — catalog slugs are name-aligned back to the Declared names via each IO's
|
|
57
|
+
* Configuration.recordTypeSlug so a slug never duplicates an existing Declared object; unknown slugs pass
|
|
58
|
+
* through as new customs. Discovery is NON-authoritative (DiscoveryIsAuthoritative=false): because the
|
|
59
|
+
* catalog is a partial enumeration (no sublist children, slug-vs-humanized names), absence proves nothing
|
|
60
|
+
* and nothing is ever deactivated. Standard baseline lives in the Declared metadata file; a credential
|
|
61
|
+
* only ADDS the account's custom records on top.
|
|
62
|
+
*/
|
|
63
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
64
|
+
import { Metadata } from '@memberjunction/core';
|
|
65
|
+
import { IntegrationEngineBase } from '@memberjunction/integration-engine-base';
|
|
66
|
+
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, OAuth1aSigner, OAuth2TokenManager, } from '@memberjunction/integration-engine';
|
|
67
|
+
import { z } from 'zod';
|
|
68
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
69
|
+
const NS_DEFAULT_PAGE_SIZE = 1000;
|
|
70
|
+
const NS_MAX_RETRIES = 3;
|
|
71
|
+
const NS_REQUEST_TIMEOUT_MS = 90_000;
|
|
72
|
+
const NS_RECORD_BASE_PATH = '/services/rest/record/v1';
|
|
73
|
+
const NS_SUITEQL_PATH = '/services/rest/query/v1/suiteql';
|
|
74
|
+
const NS_METADATA_CATALOG_PATH = '/services/rest/record/v1/metadata-catalog';
|
|
75
|
+
const NS_SERVERTIME_PATH = '/services/rest/system/v1/serverTime';
|
|
76
|
+
// NetSuite REST media type for record bodies (per docs: application/vnd.oracle.resource+json).
|
|
77
|
+
const NS_RECORD_MEDIA_TYPE = 'application/vnd.oracle.resource+json';
|
|
78
|
+
// Concurrency-governed API (no RPS); the smallest documented tier is 5 concurrent. Stay well under it.
|
|
79
|
+
const NS_MAX_CONCURRENCY = 4;
|
|
80
|
+
// ─── Config (typed via Zod) ─────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* The per-connection config the connector reads from the credential JSON or
|
|
83
|
+
* CompanyIntegration.Configuration. AccountID is always required; the auth fields
|
|
84
|
+
* present select the auth mode.
|
|
85
|
+
*/
|
|
86
|
+
const NetSuiteConfigSchema = z.object({
|
|
87
|
+
/** Per-account id, e.g. '1234567' or '1234567_SB1'. Builds the host subdomain. */
|
|
88
|
+
AccountID: z.string().min(1),
|
|
89
|
+
/**
|
|
90
|
+
* Optional explicit host-root override (no trailing slash). When set, it is used verbatim
|
|
91
|
+
* instead of deriving `https://{accountId}.suitetalk.api.netsuite.com` from AccountID.
|
|
92
|
+
* Production normally leaves this UNSET (host derives from AccountID). It exists for a custom
|
|
93
|
+
* gateway/proxy and for pointing the connector at a mock vendor in credential-free e2e tests —
|
|
94
|
+
* the same `BaseURL`-from-config hook iMIS/Hivebrite/GrowthZone expose.
|
|
95
|
+
*/
|
|
96
|
+
HostBaseURL: z.string().url().optional(),
|
|
97
|
+
/** Explicit auth mode; inferred when absent. */
|
|
98
|
+
AuthFlow: z.enum(['oauth1-tba', 'oauth2']).optional(),
|
|
99
|
+
// OAuth 1.0a TBA
|
|
100
|
+
ConsumerKey: z.string().optional(),
|
|
101
|
+
ConsumerSecret: z.string().optional(),
|
|
102
|
+
TokenID: z.string().optional(),
|
|
103
|
+
TokenSecret: z.string().optional(),
|
|
104
|
+
// OAuth 2.0 bearer
|
|
105
|
+
BearerToken: z.string().optional(),
|
|
106
|
+
AccessToken: z.string().optional(),
|
|
107
|
+
RefreshToken: z.string().optional(),
|
|
108
|
+
ClientID: z.string().optional(),
|
|
109
|
+
ClientSecret: z.string().optional(),
|
|
110
|
+
TokenURL: z.string().optional(),
|
|
111
|
+
});
|
|
112
|
+
// ─── Connector ────────────────────────────────────────────────────────────
|
|
113
|
+
let NetSuiteConnector = class NetSuiteConnector extends BaseRESTIntegrationConnector {
|
|
114
|
+
constructor() {
|
|
115
|
+
super(...arguments);
|
|
116
|
+
this.oauth2Manager = new OAuth2TokenManager();
|
|
117
|
+
}
|
|
118
|
+
// ── Identity + capabilities ──────────────────────────────────────────
|
|
119
|
+
get IntegrationName() { return 'NetSuite'; }
|
|
120
|
+
get SupportsCreate() { return true; }
|
|
121
|
+
get SupportsUpdate() { return true; }
|
|
122
|
+
get SupportsDelete() { return true; }
|
|
123
|
+
/**
|
|
124
|
+
* NON-authoritative for deactivation (deliberately false — the T3-deadlock-safe posture).
|
|
125
|
+
*
|
|
126
|
+
* The metadata-catalog enumerates the Record-Service record types (standard + custom) — but it is
|
|
127
|
+
* NOT the complete gamut of this connector's emitted IOs: the contract retains sublist-access-path
|
|
128
|
+
* children (InvoiceItem, SalesOrderItem) that are reachable as nested graph children and NEVER
|
|
129
|
+
* appear in the record-type catalog (SublistScopeExtension). It also returns lowercase record-type
|
|
130
|
+
* SLUGS (`phonecall`) while Declared standard objects are humanized (`Phone Call`). An authoritative
|
|
131
|
+
* (deactivating) refresh would therefore wrongly Disable every Declared sublist child and any object
|
|
132
|
+
* whose catalog slug doesn't byte-match the Declared name. Per the convention rule — a partial/scoped
|
|
133
|
+
* enumeration is NEVER authoritative — absence here proves nothing, so we deactivate nothing. The
|
|
134
|
+
* standard universe is the Declared metadata baseline; a credential only ADDS the account's customs.
|
|
135
|
+
*/
|
|
136
|
+
get DiscoveryIsAuthoritative() { return false; }
|
|
137
|
+
/**
|
|
138
|
+
* NetSuite governs by CONCURRENT-request limit (5–50 by tier), not requests/sec. We cap inner
|
|
139
|
+
* concurrency conservatively below the smallest tier; the engine's adaptive controller ramps within it.
|
|
140
|
+
*/
|
|
141
|
+
get MaxConcurrencyHint() { return NS_MAX_CONCURRENCY; }
|
|
142
|
+
/** No documented RPS policy — concurrency-governed. Return null so the engine derives a conservative rate. */
|
|
143
|
+
get RateLimitPolicy() { return null; }
|
|
144
|
+
/**
|
|
145
|
+
* SuiteQL fetches in `ORDER BY lastModifiedDate` and an updated record always re-surfaces at a new,
|
|
146
|
+
* higher modstamp — so the last batch's max IS the true high-water mark. Safe for the engine to use
|
|
147
|
+
* the returned watermark to narrow the next incremental.
|
|
148
|
+
*/
|
|
149
|
+
get MonotonicWatermark() { return true; }
|
|
150
|
+
/**
|
|
151
|
+
* For watermark-less objects, NetSuite's universal numeric `id` is a stable monotonic ordering key
|
|
152
|
+
* usable for keyset/seek resume.
|
|
153
|
+
*/
|
|
154
|
+
StableOrderingKey(_objectName) { return 'id'; }
|
|
155
|
+
/** Parse NetSuite's 429 Retry-After (seconds) into ms; concurrency limit errors carry no precise hint. */
|
|
156
|
+
ExtractRetryAfterMs(error) {
|
|
157
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
158
|
+
const m = msg.match(/retry-after[:\s]+(\d+)/i);
|
|
159
|
+
if (m)
|
|
160
|
+
return parseInt(m[1], 10) * 1000;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
// ── Config / credential parsing ──────────────────────────────────────
|
|
164
|
+
async ParseConfig(companyIntegration, contextUser, provider) {
|
|
165
|
+
// Credential JSON takes precedence over the non-secret Configuration JSON.
|
|
166
|
+
if (companyIntegration.CredentialID) {
|
|
167
|
+
const md = provider ?? new Metadata();
|
|
168
|
+
const cred = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
169
|
+
const loaded = await cred.Load(companyIntegration.CredentialID);
|
|
170
|
+
if (loaded && cred.Values) {
|
|
171
|
+
return this.ValidateConfig(JSON.parse(cred.Values));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (companyIntegration.Configuration) {
|
|
175
|
+
return this.ValidateConfig(JSON.parse(companyIntegration.Configuration));
|
|
176
|
+
}
|
|
177
|
+
throw new Error('NetSuite connector requires a CredentialID or Configuration JSON carrying AccountID + auth credentials.');
|
|
178
|
+
}
|
|
179
|
+
ValidateConfig(raw) {
|
|
180
|
+
const parsed = NetSuiteConfigSchema.safeParse(raw);
|
|
181
|
+
if (!parsed.success) {
|
|
182
|
+
throw new Error(`NetSuite Configuration is invalid: ${parsed.error.issues.map(i => i.message).join('; ')}`);
|
|
183
|
+
}
|
|
184
|
+
return parsed.data;
|
|
185
|
+
}
|
|
186
|
+
/** Determines the auth mode from explicit AuthFlow or the credential keys present. */
|
|
187
|
+
ResolveAuthMode(config) {
|
|
188
|
+
if (config.AuthFlow === 'oauth2')
|
|
189
|
+
return 'oauth2';
|
|
190
|
+
if (config.AuthFlow === 'oauth1-tba')
|
|
191
|
+
return 'oauth1-tba';
|
|
192
|
+
if (config.BearerToken || config.AccessToken || config.RefreshToken)
|
|
193
|
+
return 'oauth2';
|
|
194
|
+
return 'oauth1-tba';
|
|
195
|
+
}
|
|
196
|
+
// ── Auth ─────────────────────────────────────────────────────────────
|
|
197
|
+
async Authenticate(companyIntegration, contextUser) {
|
|
198
|
+
const config = await this.ParseConfig(companyIntegration, contextUser);
|
|
199
|
+
const mode = this.ResolveAuthMode(config);
|
|
200
|
+
const subdomain = config.AccountID.toLowerCase().replace(/_/g, '-');
|
|
201
|
+
// Host derives from AccountID by default; an explicit Configuration.HostBaseURL (custom
|
|
202
|
+
// gateway / mock vendor in e2e) wins when present. Trailing slash trimmed so APIPath joins clean.
|
|
203
|
+
const hostBaseURL = (config.HostBaseURL && config.HostBaseURL.trim().length > 0)
|
|
204
|
+
? config.HostBaseURL.replace(/\/+$/, '')
|
|
205
|
+
: `https://${subdomain}.suitetalk.api.netsuite.com`;
|
|
206
|
+
const realm = config.AccountID.replace(/-/g, '_').toUpperCase();
|
|
207
|
+
let bearerToken = '';
|
|
208
|
+
if (mode === 'oauth2') {
|
|
209
|
+
bearerToken = await this.ResolveBearerToken(config);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
this.AssertTBACredentials(config);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
Token: bearerToken,
|
|
216
|
+
TokenType: mode === 'oauth2' ? 'Bearer' : 'OAuth1',
|
|
217
|
+
Config: config,
|
|
218
|
+
Mode: mode,
|
|
219
|
+
HostBaseURL: hostBaseURL,
|
|
220
|
+
Realm: realm,
|
|
221
|
+
BearerToken: bearerToken,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
AssertTBACredentials(config) {
|
|
225
|
+
if (!config.ConsumerKey || !config.ConsumerSecret || !config.TokenID || !config.TokenSecret) {
|
|
226
|
+
throw new Error('NetSuite TBA (OAuth 1.0a) requires ConsumerKey, ConsumerSecret, TokenID, and TokenSecret.');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Resolves a bearer token for OAuth2 mode. A pre-minted BearerToken/AccessToken is used directly
|
|
231
|
+
* (NetSuite OAuth2 access tokens are supplied by the operator). When a RefreshToken + TokenURL +
|
|
232
|
+
* client credentials are present, the shared OAuth2TokenManager refreshes — crypto-free token round-trip.
|
|
233
|
+
*/
|
|
234
|
+
async ResolveBearerToken(config) {
|
|
235
|
+
const direct = config.BearerToken ?? config.AccessToken;
|
|
236
|
+
if (config.RefreshToken && config.TokenURL && config.ClientID && config.ClientSecret) {
|
|
237
|
+
const token = await this.oauth2Manager.GetAccessToken({
|
|
238
|
+
TokenURL: config.TokenURL,
|
|
239
|
+
ClientId: config.ClientID,
|
|
240
|
+
ClientSecret: config.ClientSecret,
|
|
241
|
+
RefreshToken: config.RefreshToken,
|
|
242
|
+
}, 'refresh_token');
|
|
243
|
+
return token.AccessToken;
|
|
244
|
+
}
|
|
245
|
+
if (direct)
|
|
246
|
+
return direct;
|
|
247
|
+
throw new Error('NetSuite OAuth2 mode requires either a BearerToken/AccessToken, or RefreshToken + TokenURL + ClientID + ClientSecret.');
|
|
248
|
+
}
|
|
249
|
+
// ── Headers ──────────────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Base headers WITHOUT an Authorization value — the auth header is per-request (OAuth 1.0a signs
|
|
252
|
+
* each URL+method) so it's injected in MakeHTTPRequest, not here. The base class calls BuildHeaders
|
|
253
|
+
* for its generic CRUD / pagination GETs; MakeHTTPRequest stamps the signed/bearer Authorization.
|
|
254
|
+
*/
|
|
255
|
+
BuildHeaders(_auth) {
|
|
256
|
+
return {
|
|
257
|
+
'Accept': 'application/json',
|
|
258
|
+
'User-Agent': 'MemberJunction-Integration/1.0',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Builds the Authorization header for a specific URL + method (TBA signs per request; OAuth2 is static). */
|
|
262
|
+
BuildAuthorizationHeader(auth, url, method) {
|
|
263
|
+
if (auth.Mode === 'oauth2') {
|
|
264
|
+
return `Bearer ${auth.BearerToken}`;
|
|
265
|
+
}
|
|
266
|
+
return OAuth1aSigner.BuildAuthorizationHeader({
|
|
267
|
+
ConsumerKey: auth.Config.ConsumerKey,
|
|
268
|
+
ConsumerSecret: auth.Config.ConsumerSecret,
|
|
269
|
+
TokenId: auth.Config.TokenID,
|
|
270
|
+
TokenSecret: auth.Config.TokenSecret,
|
|
271
|
+
Method: method,
|
|
272
|
+
Url: url,
|
|
273
|
+
Realm: auth.Realm,
|
|
274
|
+
SignatureMethod: 'HMAC-SHA256',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// ── URL building ─────────────────────────────────────────────────────
|
|
278
|
+
/** Base URL = host root; the IO's APIPath already carries the full /services/rest/... path. */
|
|
279
|
+
GetBaseURL(_companyIntegration, auth) {
|
|
280
|
+
return auth.HostBaseURL;
|
|
281
|
+
}
|
|
282
|
+
// ── HTTP transport ───────────────────────────────────────────────────
|
|
283
|
+
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
284
|
+
const nsAuth = auth;
|
|
285
|
+
const effectiveHeaders = { ...headers, 'Authorization': this.BuildAuthorizationHeader(nsAuth, url, method) };
|
|
286
|
+
if (body !== undefined && method !== 'GET' && method !== 'DELETE') {
|
|
287
|
+
// Record bodies use NetSuite's vendor media type unless a caller (SuiteQL) set its own.
|
|
288
|
+
if (!effectiveHeaders['Content-Type'])
|
|
289
|
+
effectiveHeaders['Content-Type'] = NS_RECORD_MEDIA_TYPE;
|
|
290
|
+
}
|
|
291
|
+
for (let attempt = 0; attempt <= NS_MAX_RETRIES; attempt++) {
|
|
292
|
+
let response;
|
|
293
|
+
try {
|
|
294
|
+
const opts = {
|
|
295
|
+
method,
|
|
296
|
+
headers: effectiveHeaders,
|
|
297
|
+
signal: AbortSignal.timeout(NS_REQUEST_TIMEOUT_MS),
|
|
298
|
+
};
|
|
299
|
+
if (body !== undefined && method !== 'GET' && method !== 'DELETE') {
|
|
300
|
+
opts.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
301
|
+
}
|
|
302
|
+
response = await fetch(url, opts);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (attempt < NS_MAX_RETRIES && this.IsTransientNetworkError(err)) {
|
|
306
|
+
await this.Sleep(this.BackoffDelay(attempt));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
// Concurrency-limit rejection (429) or transient 5xx — back off + retry within budget.
|
|
312
|
+
if ((response.status === 429 || response.status >= 500) && attempt < NS_MAX_RETRIES) {
|
|
313
|
+
const retryAfter = this.ParseRetryAfterHeader(response);
|
|
314
|
+
await this.Sleep(retryAfter ?? this.BackoffDelay(attempt));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
return this.BuildRESTResponse(response);
|
|
318
|
+
}
|
|
319
|
+
throw new Error(`NetSuite request failed after ${NS_MAX_RETRIES + 1} attempts: ${url}`);
|
|
320
|
+
}
|
|
321
|
+
async BuildRESTResponse(response) {
|
|
322
|
+
const hdrs = {};
|
|
323
|
+
response.headers.forEach((v, k) => { hdrs[k.toLowerCase()] = v; });
|
|
324
|
+
const ct = hdrs['content-type'] ?? '';
|
|
325
|
+
let parsed;
|
|
326
|
+
if (ct.includes('json')) {
|
|
327
|
+
const text = await response.text();
|
|
328
|
+
parsed = text.length > 0 ? JSON.parse(text) : {};
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
parsed = await response.text();
|
|
332
|
+
}
|
|
333
|
+
return { Status: response.status, Body: parsed, Headers: hdrs };
|
|
334
|
+
}
|
|
335
|
+
ParseRetryAfterHeader(response) {
|
|
336
|
+
const ra = response.headers.get('retry-after');
|
|
337
|
+
if (!ra)
|
|
338
|
+
return undefined;
|
|
339
|
+
const secs = parseInt(ra, 10);
|
|
340
|
+
return Number.isFinite(secs) ? secs * 1000 : undefined;
|
|
341
|
+
}
|
|
342
|
+
IsTransientNetworkError(err) {
|
|
343
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
344
|
+
return /econnreset|etimedout|enotfound|eai_again|socket hang up|network|fetch failed|timeout/i.test(msg);
|
|
345
|
+
}
|
|
346
|
+
BackoffDelay(attempt) {
|
|
347
|
+
return Math.min(1000 * Math.pow(2, attempt) + Math.floor(Math.random() * 500), 30_000);
|
|
348
|
+
}
|
|
349
|
+
Sleep(ms) {
|
|
350
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
351
|
+
}
|
|
352
|
+
// ── Response parsing ─────────────────────────────────────────────────
|
|
353
|
+
/** Strips the NetSuite HATEOAS envelope: { items: [...] } → the items array; bare object → [object]. */
|
|
354
|
+
NormalizeResponse(rawBody, _responseDataKey) {
|
|
355
|
+
if (Array.isArray(rawBody))
|
|
356
|
+
return rawBody;
|
|
357
|
+
if (!rawBody || typeof rawBody !== 'object')
|
|
358
|
+
return [];
|
|
359
|
+
const body = rawBody;
|
|
360
|
+
if (Array.isArray(body.items))
|
|
361
|
+
return body.items;
|
|
362
|
+
// A single-record GET returns the record object directly (with a `links` HATEOAS array).
|
|
363
|
+
if (Object.keys(body).length > 0)
|
|
364
|
+
return [body];
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
/** Offset pagination: hasMore flag + offset advance (or the next-link href when present). */
|
|
368
|
+
ExtractPaginationInfo(rawBody, _paginationType, _currentPage, currentOffset, pageSize) {
|
|
369
|
+
const body = (rawBody && typeof rawBody === 'object') ? rawBody : {};
|
|
370
|
+
const records = this.NormalizeResponse(rawBody, null);
|
|
371
|
+
// Prefer the explicit hasMore flag; fall back to "got a full page".
|
|
372
|
+
const hasMore = body.hasMore ?? (records.length >= (pageSize || NS_DEFAULT_PAGE_SIZE));
|
|
373
|
+
const nextOffset = this.NextOffsetFromLinks(body) ?? currentOffset + records.length;
|
|
374
|
+
return {
|
|
375
|
+
HasMore: hasMore,
|
|
376
|
+
NextOffset: nextOffset,
|
|
377
|
+
TotalRecords: body.totalResults,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/** Reads the `offset` query param from the HATEOAS rel='next' link when the envelope provides one. */
|
|
381
|
+
NextOffsetFromLinks(body) {
|
|
382
|
+
const next = body.links?.find(l => l.rel === 'next');
|
|
383
|
+
if (!next?.href)
|
|
384
|
+
return undefined;
|
|
385
|
+
const qIdx = next.href.indexOf('?');
|
|
386
|
+
if (qIdx === -1)
|
|
387
|
+
return undefined;
|
|
388
|
+
const off = new URLSearchParams(next.href.substring(qIdx + 1)).get('offset');
|
|
389
|
+
const n = off != null ? parseInt(off, 10) : NaN;
|
|
390
|
+
return Number.isFinite(n) ? n : undefined;
|
|
391
|
+
}
|
|
392
|
+
// ── TestConnection ───────────────────────────────────────────────────
|
|
393
|
+
/** Validates credentials + host by running a trivial SuiteQL probe (no record dependency). */
|
|
394
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
395
|
+
try {
|
|
396
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
397
|
+
// serverTime is the cheapest authenticated read and seeds the incremental watermark.
|
|
398
|
+
const url = `${auth.HostBaseURL}${NS_SERVERTIME_PATH}`;
|
|
399
|
+
const headers = this.BuildHeaders(auth);
|
|
400
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
401
|
+
if (response.Status >= 200 && response.Status < 300) {
|
|
402
|
+
const st = response.Body;
|
|
403
|
+
const t = st?.serverTime ?? st?.time ?? 'unknown';
|
|
404
|
+
return { Success: true, Message: `Connected to NetSuite (${auth.Mode}); server time ${t}` };
|
|
405
|
+
}
|
|
406
|
+
if (response.Status === 401 || response.Status === 403) {
|
|
407
|
+
return { Success: false, Message: `NetSuite authentication failed: HTTP ${response.Status}` };
|
|
408
|
+
}
|
|
409
|
+
return { Success: false, Message: `NetSuite returned HTTP ${response.Status} from ${NS_SERVERTIME_PATH}` };
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
return { Success: false, Message: err instanceof Error ? err.message : String(err) };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ── Discovery (auth-gated metadata-catalog MECHANISM — no baked catalog) ──
|
|
416
|
+
/**
|
|
417
|
+
* Discovers objects by UNIONING the Declared baseline with the live metadata-catalog (case-2
|
|
418
|
+
* auth-gated discovery). The Declared metadata (200+ standard record types + the sublist-access-path
|
|
419
|
+
* children that the catalog never lists) is the floor — it is ALWAYS surfaced so a refresh can't drop
|
|
420
|
+
* it. The catalog's record-type SLUGS (`phonecall`) are name-aligned back to the Declared name via the
|
|
421
|
+
* cached Configuration.recordTypeSlug so a slug never duplicates an existing Declared object; an
|
|
422
|
+
* unknown slug passes through as a NEW custom record. Net: standard universe always present + tenant
|
|
423
|
+
* customs added. Non-authoritative (DiscoveryIsAuthoritative=false), so no object is ever deactivated.
|
|
424
|
+
*/
|
|
425
|
+
async DiscoverObjects(companyIntegration, contextUser) {
|
|
426
|
+
const declared = await this.GetDeclaredObjects(companyIntegration, contextUser);
|
|
427
|
+
const slugToDeclaredName = this.BuildSlugToDeclaredNameMap(companyIntegration.IntegrationID);
|
|
428
|
+
const declaredNamesLower = new Set(declared.map(o => o.Name.toLowerCase()));
|
|
429
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
430
|
+
const url = `${auth.HostBaseURL}${NS_METADATA_CATALOG_PATH}`;
|
|
431
|
+
const headers = { ...this.BuildHeaders(auth), 'Accept': 'application/schema+json' };
|
|
432
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
433
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
434
|
+
throw new Error(`NetSuite metadata-catalog returned HTTP ${response.Status} — cannot enumerate record types.`);
|
|
435
|
+
}
|
|
436
|
+
const slugs = this.ExtractCatalogNames(response.Body);
|
|
437
|
+
const out = [...declared];
|
|
438
|
+
for (const slug of slugs) {
|
|
439
|
+
// Name-align to the Declared name this slug maps to; for an UNKNOWN slug (a new custom record)
|
|
440
|
+
// keep the slug VERBATIM as the Name — the slug is the API-addressable record-type identity, so
|
|
441
|
+
// it must survive untouched for the discovered custom's CRUD/SuiteQL paths. (Label is humanized.)
|
|
442
|
+
const name = slugToDeclaredName.get(slug.toLowerCase()) ?? slug;
|
|
443
|
+
if (declaredNamesLower.has(name.toLowerCase()))
|
|
444
|
+
continue; // already in the Declared floor
|
|
445
|
+
declaredNamesLower.add(name.toLowerCase());
|
|
446
|
+
out.push({
|
|
447
|
+
Name: name,
|
|
448
|
+
Label: this.HumanizeName(slug),
|
|
449
|
+
Description: `NetSuite ${this.HumanizeName(slug)} record`,
|
|
450
|
+
SupportsIncrementalSync: true,
|
|
451
|
+
SupportsWrite: true,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* The Declared (cached) baseline objects — the floor DiscoverObjects always surfaces. Seam over the
|
|
458
|
+
* base class's cache read so the union logic is unit-testable without a DB-backed engine.
|
|
459
|
+
*/
|
|
460
|
+
GetDeclaredObjects(companyIntegration, contextUser) {
|
|
461
|
+
return super.DiscoverObjects(companyIntegration, contextUser);
|
|
462
|
+
}
|
|
463
|
+
/** Maps each Declared IO's Configuration.recordTypeSlug → its Declared Name (for catalog name-alignment). */
|
|
464
|
+
BuildSlugToDeclaredNameMap(integrationID) {
|
|
465
|
+
const map = new Map();
|
|
466
|
+
const objects = IntegrationEngineBase.Instance.GetActiveIntegrationObjects(integrationID);
|
|
467
|
+
for (const obj of objects) {
|
|
468
|
+
const slug = this.ReadRecordTypeSlug(obj.Configuration);
|
|
469
|
+
if (slug)
|
|
470
|
+
map.set(slug.toLowerCase(), obj.Name);
|
|
471
|
+
}
|
|
472
|
+
return map;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Resolves the metadata-catalog slug for an object name: the cached IO's Configuration.recordTypeSlug
|
|
476
|
+
* when present (the authoritative slug, e.g. `phonecall` for `Phone Call`), else a name collapse.
|
|
477
|
+
*/
|
|
478
|
+
ResolveRecordTypeSlug(integrationID, objectName) {
|
|
479
|
+
try {
|
|
480
|
+
const obj = this.GetCachedObject(integrationID, objectName);
|
|
481
|
+
const slug = this.ReadRecordTypeSlug(obj.Configuration);
|
|
482
|
+
if (slug)
|
|
483
|
+
return slug;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// object not in cache (a freshly-discovered custom) — fall through to name collapse
|
|
487
|
+
}
|
|
488
|
+
return objectName.toLowerCase().replace(/\s+/g, '');
|
|
489
|
+
}
|
|
490
|
+
/** Tolerantly reads Configuration.recordTypeSlug from an IO's Configuration JSON. */
|
|
491
|
+
ReadRecordTypeSlug(configuration) {
|
|
492
|
+
if (!configuration)
|
|
493
|
+
return undefined;
|
|
494
|
+
try {
|
|
495
|
+
const cfg = JSON.parse(configuration);
|
|
496
|
+
return typeof cfg.recordTypeSlug === 'string' ? cfg.recordTypeSlug : undefined;
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** Pulls record-type names from the catalog's `items[].name` or `links[].href` trailing segment. */
|
|
503
|
+
ExtractCatalogNames(body) {
|
|
504
|
+
const out = [];
|
|
505
|
+
for (const it of body.items ?? []) {
|
|
506
|
+
if (it.name)
|
|
507
|
+
out.push(it.name);
|
|
508
|
+
}
|
|
509
|
+
if (out.length === 0) {
|
|
510
|
+
for (const lk of body.links ?? []) {
|
|
511
|
+
if (lk.rel === 'self' || !lk.href)
|
|
512
|
+
continue;
|
|
513
|
+
const seg = lk.href.split('/').filter(Boolean).pop();
|
|
514
|
+
if (seg)
|
|
515
|
+
out.push(seg);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Discovers fields for a record type from the metadata-catalog JSON-schema for that type. The schema
|
|
522
|
+
* `properties` map → fields; `required` → IsRequired; `readOnly`/computed markers → IsReadOnly; the
|
|
523
|
+
* universal numeric `id` is the PK. Falls back to the cached Declared fields when the catalog can't
|
|
524
|
+
* describe this type.
|
|
525
|
+
*/
|
|
526
|
+
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
527
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
528
|
+
// Prefer the IO's Configuration.recordTypeSlug (authoritative slug); else collapse the name.
|
|
529
|
+
const slug = this.ResolveRecordTypeSlug(companyIntegration.IntegrationID, objectName);
|
|
530
|
+
const url = `${auth.HostBaseURL}${NS_METADATA_CATALOG_PATH}/${encodeURIComponent(slug)}`;
|
|
531
|
+
const headers = { ...this.BuildHeaders(auth), 'Accept': 'application/schema+json' };
|
|
532
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
533
|
+
if (response.Status >= 200 && response.Status < 300) {
|
|
534
|
+
const parsed = this.ParseFieldSchema(response.Body);
|
|
535
|
+
if (parsed.length > 0)
|
|
536
|
+
return parsed;
|
|
537
|
+
}
|
|
538
|
+
// Could not describe via catalog — defer to the Declared metadata (cache) the base class returns.
|
|
539
|
+
return super.DiscoverFields(companyIntegration, objectName, contextUser);
|
|
540
|
+
}
|
|
541
|
+
/** Maps a JSON-schema describe body to ExternalFieldSchema[] (provable-only: only stated flags). */
|
|
542
|
+
ParseFieldSchema(body) {
|
|
543
|
+
if (!body || typeof body !== 'object')
|
|
544
|
+
return [];
|
|
545
|
+
const schema = body;
|
|
546
|
+
if (!schema.properties)
|
|
547
|
+
return [];
|
|
548
|
+
const required = new Set(schema.required ?? []);
|
|
549
|
+
const fields = [];
|
|
550
|
+
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
551
|
+
const isId = name === 'id';
|
|
552
|
+
fields.push({
|
|
553
|
+
Name: name,
|
|
554
|
+
Label: String(prop['title'] ?? this.HumanizeName(name)),
|
|
555
|
+
Description: typeof prop['description'] === 'string' ? prop['description'] : undefined,
|
|
556
|
+
DataType: this.MapSchemaType(prop),
|
|
557
|
+
IsRequired: required.has(name),
|
|
558
|
+
IsPrimaryKey: isId || undefined,
|
|
559
|
+
IsUniqueKey: isId,
|
|
560
|
+
IsReadOnly: prop['readOnly'] === true || isId,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (!fields.some(f => f.Name === 'id')) {
|
|
564
|
+
fields.unshift({
|
|
565
|
+
Name: 'id', Label: 'Internal ID', Description: 'NetSuite internal record ID',
|
|
566
|
+
DataType: 'string', IsRequired: false, IsPrimaryKey: true, IsUniqueKey: true, IsReadOnly: true,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return fields;
|
|
570
|
+
}
|
|
571
|
+
/** Maps a JSON-schema property to a generic source type. */
|
|
572
|
+
MapSchemaType(prop) {
|
|
573
|
+
const t = String(prop['type'] ?? '').toLowerCase();
|
|
574
|
+
const fmt = String(prop['format'] ?? '').toLowerCase();
|
|
575
|
+
if (fmt.includes('date') || fmt.includes('time'))
|
|
576
|
+
return 'datetime';
|
|
577
|
+
if (t === 'integer')
|
|
578
|
+
return 'number';
|
|
579
|
+
if (t === 'number')
|
|
580
|
+
return 'decimal';
|
|
581
|
+
if (t === 'boolean')
|
|
582
|
+
return 'boolean';
|
|
583
|
+
return 'string';
|
|
584
|
+
}
|
|
585
|
+
HumanizeName(name) {
|
|
586
|
+
return name
|
|
587
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
588
|
+
.replace(/[_-]/g, ' ')
|
|
589
|
+
.trim()
|
|
590
|
+
.split(/\s+/)
|
|
591
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
592
|
+
.join(' ') || name;
|
|
593
|
+
}
|
|
594
|
+
// ── FetchChanges — SuiteQL (the one idiosyncratic override) ──────────
|
|
595
|
+
//
|
|
596
|
+
// OVERRIDE RATIONALE: NetSuite reads run through SuiteQL (POST with SQL in the body + the required
|
|
597
|
+
// `Prefer: transient` header) so the lastModifiedDate incremental predicate can be expressed — a
|
|
598
|
+
// shape the base class's generic GET path cannot produce. CRUD stays on the generic per-operation path.
|
|
599
|
+
async FetchChanges(ctx) {
|
|
600
|
+
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
601
|
+
const fields = this.GetCachedFields(obj.ID);
|
|
602
|
+
const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
|
|
603
|
+
const cfg = this.ReadIOConfig(obj);
|
|
604
|
+
// SuiteQL table = the IO's configured suiteQLTable; fall back to the catalog slug (collapsed name).
|
|
605
|
+
const table = cfg.suiteQLTable ?? this.ResolveRecordTypeSlug(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
606
|
+
const watermarkField = obj.IncrementalWatermarkField ?? 'lastModifiedDate';
|
|
607
|
+
const offset = ctx.CurrentOffset ?? 0;
|
|
608
|
+
const pageSize = obj.DefaultPageSize ?? cfg.defaultPageSize ?? NS_DEFAULT_PAGE_SIZE;
|
|
609
|
+
const sql = this.BuildSuiteQL(table, watermarkField, ctx.WatermarkValue);
|
|
610
|
+
const url = `${auth.HostBaseURL}${NS_SUITEQL_PATH}?limit=${pageSize}&offset=${offset}`;
|
|
611
|
+
// SuiteQL bodies are plain JSON and REQUIRE the `Prefer: transient` header.
|
|
612
|
+
const headers = {
|
|
613
|
+
...this.BuildHeaders(auth),
|
|
614
|
+
'Content-Type': 'application/json',
|
|
615
|
+
'Prefer': 'transient',
|
|
616
|
+
};
|
|
617
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, { q: sql });
|
|
618
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
619
|
+
throw new Error(`NetSuite SuiteQL read failed for "${ctx.ObjectName}": HTTP ${response.Status} — ${this.PreviewBody(response.Body)}`);
|
|
620
|
+
}
|
|
621
|
+
const body = response.Body;
|
|
622
|
+
const rawRecords = this.NormalizeResponse(body, null);
|
|
623
|
+
const pkFieldNames = fields.filter(f => f.IsPrimaryKey).map(f => f.Name);
|
|
624
|
+
const records = rawRecords.map(raw => this.BuildExternalRecord(raw, ctx.ObjectName, pkFieldNames));
|
|
625
|
+
const hasMore = body.hasMore ?? (rawRecords.length >= pageSize);
|
|
626
|
+
// Track the max watermark and persist it ONLY when the full result set has been drained
|
|
627
|
+
// (partial-failure semantics: a mid-iteration stop leaves the watermark unchanged).
|
|
628
|
+
const newWatermark = !hasMore ? this.MaxWatermark(rawRecords, watermarkField) : undefined;
|
|
629
|
+
return {
|
|
630
|
+
Records: records,
|
|
631
|
+
HasMore: hasMore,
|
|
632
|
+
NextOffset: offset + rawRecords.length,
|
|
633
|
+
NewWatermarkValue: newWatermark,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/** Builds the SuiteQL SELECT, applying the incremental watermark predicate when a watermark exists. */
|
|
637
|
+
BuildSuiteQL(table, watermarkField, watermark) {
|
|
638
|
+
const where = watermark
|
|
639
|
+
? ` WHERE ${watermarkField} > ${this.QuoteSuiteQLDate(watermark)}`
|
|
640
|
+
: '';
|
|
641
|
+
return `SELECT * FROM ${table}${where} ORDER BY ${watermarkField}`;
|
|
642
|
+
}
|
|
643
|
+
/** Quotes a watermark value for SuiteQL — wraps a NetSuite datetime in TO_DATE/quotes, escaping quotes. */
|
|
644
|
+
QuoteSuiteQLDate(value) {
|
|
645
|
+
const escaped = value.replace(/'/g, "''");
|
|
646
|
+
return `'${escaped}'`;
|
|
647
|
+
}
|
|
648
|
+
/** Returns the maximum value of the watermark field across the batch (string compare on ISO timestamps). */
|
|
649
|
+
MaxWatermark(records, field) {
|
|
650
|
+
let max;
|
|
651
|
+
for (const r of records) {
|
|
652
|
+
const v = r[field];
|
|
653
|
+
if (typeof v === 'string' && v.length > 0 && (!max || v > max))
|
|
654
|
+
max = v;
|
|
655
|
+
}
|
|
656
|
+
return max;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Builds an ExternalRecord carrying the FULL source record in Fields (full-record pass-through, so the
|
|
660
|
+
* framework's custom-column capture sees every key) and the universal `id` as the ExternalID. Mirrors
|
|
661
|
+
* the base class's composite-PK + content-hash fallback at the connector boundary for hand-built records.
|
|
662
|
+
*/
|
|
663
|
+
BuildExternalRecord(raw, objectType, pkFieldNames) {
|
|
664
|
+
const keys = pkFieldNames.length > 0 ? pkFieldNames : ['id'];
|
|
665
|
+
const parts = keys.map(k => {
|
|
666
|
+
const v = raw[k];
|
|
667
|
+
return v == null ? '' : String(v);
|
|
668
|
+
});
|
|
669
|
+
const allPresent = parts.every(p => p.length > 0);
|
|
670
|
+
const externalID = allPresent ? parts.join('|') : String(raw['id'] ?? '');
|
|
671
|
+
return {
|
|
672
|
+
ExternalID: externalID,
|
|
673
|
+
ObjectType: objectType,
|
|
674
|
+
Fields: raw,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/** Reads the IO's Configuration JSON (suiteQLTable, defaultPageSize) tolerantly. */
|
|
678
|
+
ReadIOConfig(obj) {
|
|
679
|
+
if (!obj.Configuration)
|
|
680
|
+
return {};
|
|
681
|
+
try {
|
|
682
|
+
const cfg = JSON.parse(obj.Configuration);
|
|
683
|
+
return {
|
|
684
|
+
suiteQLTable: typeof cfg.suiteQLTable === 'string' ? cfg.suiteQLTable : undefined,
|
|
685
|
+
defaultPageSize: typeof cfg.defaultPageSize === 'number' ? cfg.defaultPageSize : undefined,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
PreviewBody(body) {
|
|
693
|
+
const s = typeof body === 'string' ? body : JSON.stringify(body);
|
|
694
|
+
return s ? s.substring(0, 300) : '';
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
NetSuiteConnector = __decorate([
|
|
698
|
+
RegisterClass(BaseIntegrationConnector, '@memberjunction/connector-netsuite')
|
|
699
|
+
], NetSuiteConnector);
|
|
700
|
+
export { NetSuiteConnector };
|
|
701
|
+
// Tree-shaking prevention — REQUIRED so @RegisterClass survives bundling.
|
|
702
|
+
export function LoadNetSuiteConnector() { }
|
|
703
|
+
//# sourceMappingURL=NetSuiteConnector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetSuiteConnector.js","sourceRoot":"","sources":["../src/NetSuiteConnector.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './NetSuiteConnector.js';
|
|
2
|
+
/** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
|
|
3
|
+
* this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
|
|
4
|
+
export declare function registerConnector(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './NetSuiteConnector.js';
|
|
2
|
+
/** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
|
|
3
|
+
* this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
|
|
4
|
+
export function registerConnector() { }
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AAEvC;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/connector-netsuite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MemberJunction NetSuite connector.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && tsc-alias -f",
|
|
13
|
+
"test": "vitest run --passWithNoTests"
|
|
14
|
+
},
|
|
15
|
+
"author": "MemberJunction.com",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@memberjunction/core": ">=5.42.0 <6.0.0",
|
|
19
|
+
"@memberjunction/core-entities": ">=5.42.0 <6.0.0",
|
|
20
|
+
"@memberjunction/global": ">=5.42.0 <6.0.0",
|
|
21
|
+
"@memberjunction/integration-engine": ">=5.42.0 <6.0.0",
|
|
22
|
+
"@memberjunction/integration-engine-base": ">=5.42.0 <6.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"zod": "~3.24.4"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "24.10.11",
|
|
29
|
+
"tsc-alias": "^1.8.16",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vitest": "^4.0.18",
|
|
32
|
+
"@memberjunction/core": "^5.42.0",
|
|
33
|
+
"@memberjunction/core-entities": "^5.42.0",
|
|
34
|
+
"@memberjunction/global": "^5.42.0",
|
|
35
|
+
"@memberjunction/integration-engine": "^5.42.0",
|
|
36
|
+
"@memberjunction/integration-engine-base": "^5.42.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/MemberJunction/Integrations"
|
|
41
|
+
}
|
|
42
|
+
}
|