@suiteportal/connector 0.2.1 → 0.3.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/index.cjs +137 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -3
- package/dist/index.d.ts +96 -3
- package/dist/index.js +133 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -25,7 +25,9 @@ __export(index_exports, {
|
|
|
25
25
|
NetSuiteError: () => NetSuiteError,
|
|
26
26
|
RateLimitError: () => RateLimitError,
|
|
27
27
|
RateLimiter: () => RateLimiter,
|
|
28
|
+
RequestThrottler: () => RequestThrottler,
|
|
28
29
|
TimeoutError: () => TimeoutError,
|
|
30
|
+
TokenBucket: () => TokenBucket,
|
|
29
31
|
buildAuthorizationHeader: () => buildAuthorizationHeader,
|
|
30
32
|
buildSignatureBaseString: () => buildSignatureBaseString,
|
|
31
33
|
createRecord: () => createRecord,
|
|
@@ -42,7 +44,9 @@ __export(index_exports, {
|
|
|
42
44
|
percentEncode: () => percentEncode,
|
|
43
45
|
resolveConfig: () => resolveConfig,
|
|
44
46
|
signHmacSha256: () => signHmacSha256,
|
|
47
|
+
transformRecord: () => transformRecord,
|
|
45
48
|
updateRecord: () => updateRecord,
|
|
49
|
+
upsertRecord: () => upsertRecord,
|
|
46
50
|
withRetry: () => withRetry
|
|
47
51
|
});
|
|
48
52
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -138,6 +142,8 @@ function resolveConfig(config) {
|
|
|
138
142
|
timeout: config.timeout ?? DEFAULTS.timeout,
|
|
139
143
|
concurrency: config.concurrency ?? DEFAULTS.concurrency,
|
|
140
144
|
maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,
|
|
145
|
+
requestsPerSecond: config.requestsPerSecond,
|
|
146
|
+
burstSize: config.burstSize,
|
|
141
147
|
baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId)
|
|
142
148
|
};
|
|
143
149
|
}
|
|
@@ -230,6 +236,77 @@ var RateLimiter = class {
|
|
|
230
236
|
}
|
|
231
237
|
};
|
|
232
238
|
|
|
239
|
+
// src/http/token-bucket.ts
|
|
240
|
+
var TokenBucket = class {
|
|
241
|
+
tokens;
|
|
242
|
+
maxTokens;
|
|
243
|
+
refillRate;
|
|
244
|
+
// tokens per ms
|
|
245
|
+
lastRefill;
|
|
246
|
+
constructor(options) {
|
|
247
|
+
this.maxTokens = options.burstSize ?? options.requestsPerSecond;
|
|
248
|
+
this.refillRate = options.requestsPerSecond / 1e3;
|
|
249
|
+
this.tokens = this.maxTokens;
|
|
250
|
+
this.lastRefill = Date.now();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Acquire a token, waiting if necessary.
|
|
254
|
+
* Resolves immediately if a token is available, otherwise
|
|
255
|
+
* delays until the bucket refills enough for one token.
|
|
256
|
+
*/
|
|
257
|
+
async acquire() {
|
|
258
|
+
this.refill();
|
|
259
|
+
if (this.tokens >= 1) {
|
|
260
|
+
this.tokens--;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const waitMs = (1 - this.tokens) / this.refillRate;
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
265
|
+
this.refill();
|
|
266
|
+
this.tokens--;
|
|
267
|
+
}
|
|
268
|
+
/** Number of tokens currently available. */
|
|
269
|
+
get availableTokens() {
|
|
270
|
+
this.refill();
|
|
271
|
+
return this.tokens;
|
|
272
|
+
}
|
|
273
|
+
refill() {
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const elapsed = now - this.lastRefill;
|
|
276
|
+
if (elapsed <= 0) return;
|
|
277
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
278
|
+
this.lastRefill = now;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/http/request-throttler.ts
|
|
283
|
+
var RequestThrottler = class {
|
|
284
|
+
semaphore;
|
|
285
|
+
tokenBucket;
|
|
286
|
+
constructor(options = {}) {
|
|
287
|
+
this.semaphore = new RateLimiter(options.concurrency ?? 5);
|
|
288
|
+
this.tokenBucket = options.requestsPerSecond != null ? new TokenBucket({
|
|
289
|
+
requestsPerSecond: options.requestsPerSecond,
|
|
290
|
+
burstSize: options.burstSize
|
|
291
|
+
}) : null;
|
|
292
|
+
}
|
|
293
|
+
/** Run an async function within both rate and concurrency limits. */
|
|
294
|
+
async run(fn) {
|
|
295
|
+
if (this.tokenBucket) {
|
|
296
|
+
await this.tokenBucket.acquire();
|
|
297
|
+
}
|
|
298
|
+
return this.semaphore.run(fn);
|
|
299
|
+
}
|
|
300
|
+
/** Number of currently active tasks. */
|
|
301
|
+
get activeCount() {
|
|
302
|
+
return this.semaphore.activeCount;
|
|
303
|
+
}
|
|
304
|
+
/** Number of tasks waiting for a slot. */
|
|
305
|
+
get waitingCount() {
|
|
306
|
+
return this.semaphore.waitingCount;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
233
310
|
// src/http/retry.ts
|
|
234
311
|
async function withRetry(fn, options) {
|
|
235
312
|
const { maxRetries, baseDelay = 500, maxDelay = 3e4 } = options;
|
|
@@ -265,17 +342,22 @@ function sleep(ms) {
|
|
|
265
342
|
// src/http/client.ts
|
|
266
343
|
var NetSuiteClient = class {
|
|
267
344
|
config;
|
|
268
|
-
|
|
345
|
+
throttler;
|
|
269
346
|
constructor(config) {
|
|
270
347
|
this.config = resolveConfig(config);
|
|
271
|
-
this.
|
|
348
|
+
this.throttler = new RequestThrottler({
|
|
349
|
+
concurrency: this.config.concurrency,
|
|
350
|
+
requestsPerSecond: this.config.requestsPerSecond,
|
|
351
|
+
burstSize: this.config.burstSize
|
|
352
|
+
});
|
|
272
353
|
}
|
|
273
354
|
/** Make an authenticated request to the NetSuite REST API. */
|
|
274
355
|
async request(options) {
|
|
275
|
-
|
|
356
|
+
const shouldRetry = options.retry ?? options.method === "GET";
|
|
357
|
+
return this.throttler.run(
|
|
276
358
|
() => withRetry(
|
|
277
359
|
() => this.executeRequest(options),
|
|
278
|
-
{ maxRetries: this.config.maxRetries }
|
|
360
|
+
{ maxRetries: shouldRetry ? this.config.maxRetries : 0 }
|
|
279
361
|
)
|
|
280
362
|
);
|
|
281
363
|
}
|
|
@@ -305,8 +387,9 @@ var NetSuiteClient = class {
|
|
|
305
387
|
Accept: "application/json",
|
|
306
388
|
...options.headers
|
|
307
389
|
};
|
|
390
|
+
const timeoutMs = options.timeout ?? this.config.timeout;
|
|
308
391
|
const controller = new AbortController();
|
|
309
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
392
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
310
393
|
try {
|
|
311
394
|
const response = await fetch(url, {
|
|
312
395
|
method: options.method,
|
|
@@ -324,7 +407,7 @@ var NetSuiteClient = class {
|
|
|
324
407
|
clearTimeout(timeoutId);
|
|
325
408
|
if (error instanceof NetSuiteError) throw error;
|
|
326
409
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
327
|
-
throw new TimeoutError(
|
|
410
|
+
throw new TimeoutError(timeoutMs);
|
|
328
411
|
}
|
|
329
412
|
throw new NetSuiteError(
|
|
330
413
|
`Network error: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -385,7 +468,9 @@ async function executeSuiteQL(client, query, options) {
|
|
|
385
468
|
query: {
|
|
386
469
|
limit: limit.toString(),
|
|
387
470
|
offset: offset.toString()
|
|
388
|
-
}
|
|
471
|
+
},
|
|
472
|
+
retry: true
|
|
473
|
+
// POST-for-query is idempotent
|
|
389
474
|
});
|
|
390
475
|
const items = (response.data.items ?? []).map(({ links, ...rest }) => rest);
|
|
391
476
|
return {
|
|
@@ -437,6 +522,14 @@ async function deleteRecord(client, recordType, id) {
|
|
|
437
522
|
path: `${RECORD_BASE}/${recordType}/${id}`
|
|
438
523
|
});
|
|
439
524
|
}
|
|
525
|
+
async function upsertRecord(client, recordType, externalId, data) {
|
|
526
|
+
const response = await client.request({
|
|
527
|
+
method: "PUT",
|
|
528
|
+
path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,
|
|
529
|
+
body: data
|
|
530
|
+
});
|
|
531
|
+
return response.data;
|
|
532
|
+
}
|
|
440
533
|
async function getRecord(client, recordType, id) {
|
|
441
534
|
const response = await client.request({
|
|
442
535
|
method: "GET",
|
|
@@ -444,6 +537,39 @@ async function getRecord(client, recordType, id) {
|
|
|
444
537
|
});
|
|
445
538
|
return response.data;
|
|
446
539
|
}
|
|
540
|
+
|
|
541
|
+
// src/rest/transform.ts
|
|
542
|
+
var RECORD_BASE2 = "/services/rest/record/v1";
|
|
543
|
+
var DEFAULT_TRANSFORM_TIMEOUT = 12e4;
|
|
544
|
+
async function transformRecord(client, sourceType, sourceId, targetType, options) {
|
|
545
|
+
const response = await client.request({
|
|
546
|
+
method: "POST",
|
|
547
|
+
path: `${RECORD_BASE2}/${sourceType}/${sourceId}/!transform/${targetType}`,
|
|
548
|
+
body: options?.body ?? {},
|
|
549
|
+
timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,
|
|
550
|
+
retry: false
|
|
551
|
+
});
|
|
552
|
+
const location = response.headers.get("Location");
|
|
553
|
+
let id;
|
|
554
|
+
if (location) {
|
|
555
|
+
const match = location.match(/\/(\d+)$/);
|
|
556
|
+
if (match) {
|
|
557
|
+
id = match[1];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (!id && response.data) {
|
|
561
|
+
const rawId = response.data["id"] ?? response.data["internalid"] ?? response.data["internalId"];
|
|
562
|
+
if (rawId != null) {
|
|
563
|
+
id = String(rawId);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!id) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. Location header: ${location ?? "(none)"}`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
return { id, type: targetType };
|
|
572
|
+
}
|
|
447
573
|
// Annotate the CommonJS export names for ESM import in node:
|
|
448
574
|
0 && (module.exports = {
|
|
449
575
|
AuthError,
|
|
@@ -451,7 +577,9 @@ async function getRecord(client, recordType, id) {
|
|
|
451
577
|
NetSuiteError,
|
|
452
578
|
RateLimitError,
|
|
453
579
|
RateLimiter,
|
|
580
|
+
RequestThrottler,
|
|
454
581
|
TimeoutError,
|
|
582
|
+
TokenBucket,
|
|
455
583
|
buildAuthorizationHeader,
|
|
456
584
|
buildSignatureBaseString,
|
|
457
585
|
createRecord,
|
|
@@ -468,7 +596,9 @@ async function getRecord(client, recordType, id) {
|
|
|
468
596
|
percentEncode,
|
|
469
597
|
resolveConfig,
|
|
470
598
|
signHmacSha256,
|
|
599
|
+
transformRecord,
|
|
471
600
|
updateRecord,
|
|
601
|
+
upsertRecord,
|
|
472
602
|
withRetry
|
|
473
603
|
});
|
|
474
604
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts"],"sourcesContent":["// Core client\nexport { NetSuiteClient } from './http/client.js';\n\n// SuiteQL\nexport { executeSuiteQL } from './suiteql/executor.js';\nexport { executeSuiteQLPaginated } from './suiteql/paginator.js';\nexport type { PaginationOptions } from './suiteql/paginator.js';\n\n// Auth (exposed for advanced use / testing)\nexport { buildAuthorizationHeader } from './auth/headers.js';\nexport {\n generateOAuthSignature,\n percentEncode,\n buildSignatureBaseString,\n signHmacSha256,\n generateNonce,\n generateTimestamp,\n} from './auth/oauth.js';\n\n// REST Record API\nexport { createRecord, updateRecord, deleteRecord, getRecord } from './rest/record.js';\n\n// Config\nexport { resolveConfig, deriveBaseUrl, getRealm } from './config.js';\nexport type { ResolvedConfig } from './config.js';\n\n// Errors\nexport {\n NetSuiteError,\n AuthError,\n RateLimitError,\n TimeoutError,\n isRetryableStatus,\n} from './http/errors.js';\n\n// Rate limiter\nexport { RateLimiter } from './http/rate-limiter.js';\n\n// Retry\nexport { withRetry } from './http/retry.js';\nexport type { RetryOptions } from './http/retry.js';\n\n// Types\nexport type {\n NetSuiteConfig,\n SuiteQLRow,\n SuiteQLResult,\n SuiteQLResponse,\n OAuthParams,\n RequestOptions,\n NetSuiteResponse,\n} from './types.js';\n","import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value\n const sorted = [...params].sort((a, b) => {\n const keyCompare = a[0]!.localeCompare(b[0]!);\n return keyCompare !== 0 ? keyCompare : a[1]!.localeCompare(b[1]!);\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string\n const urlObj = new URL(params.url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n urlObj.searchParams.forEach((value, key) => {\n allParams.push([key, value]);\n });\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {\n baseUrl: string;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RateLimiter } from './rate-limiter.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly rateLimiter: RateLimiter;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.rateLimiter = new RateLimiter(this.config.concurrency);\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n return this.rateLimiter.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: this.config.maxRetries },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(this.config.timeout);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/token-bucket.ts","../src/http/request-throttler.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts","../src/rest/transform.ts"],"sourcesContent":["// Core client\nexport { NetSuiteClient } from './http/client.js';\n\n// SuiteQL\nexport { executeSuiteQL } from './suiteql/executor.js';\nexport { executeSuiteQLPaginated } from './suiteql/paginator.js';\nexport type { PaginationOptions } from './suiteql/paginator.js';\n\n// Auth (exposed for advanced use / testing)\nexport { buildAuthorizationHeader } from './auth/headers.js';\nexport {\n generateOAuthSignature,\n percentEncode,\n buildSignatureBaseString,\n signHmacSha256,\n generateNonce,\n generateTimestamp,\n} from './auth/oauth.js';\n\n// REST Record API\nexport { createRecord, updateRecord, deleteRecord, upsertRecord, getRecord } from './rest/record.js';\nexport { transformRecord } from './rest/transform.js';\nexport type { TransformOptions, TransformResult } from './rest/transform.js';\n\n// Config\nexport { resolveConfig, deriveBaseUrl, getRealm } from './config.js';\nexport type { ResolvedConfig } from './config.js';\n\n// Errors\nexport {\n NetSuiteError,\n AuthError,\n RateLimitError,\n TimeoutError,\n isRetryableStatus,\n} from './http/errors.js';\n\n// Rate limiter\nexport { RateLimiter } from './http/rate-limiter.js';\n\n// Token bucket + throttler\nexport { TokenBucket } from './http/token-bucket.js';\nexport type { TokenBucketOptions } from './http/token-bucket.js';\nexport { RequestThrottler } from './http/request-throttler.js';\nexport type { ThrottlerOptions } from './http/request-throttler.js';\n\n// Retry\nexport { withRetry } from './http/retry.js';\nexport type { RetryOptions } from './http/retry.js';\n\n// Types\nexport type {\n NetSuiteConfig,\n SuiteQLRow,\n SuiteQLResult,\n SuiteQLResponse,\n OAuthParams,\n RequestOptions,\n NetSuiteResponse,\n} from './types.js';\n","import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value\n const sorted = [...params].sort((a, b) => {\n const keyCompare = a[0]!.localeCompare(b[0]!);\n return keyCompare !== 0 ? keyCompare : a[1]!.localeCompare(b[1]!);\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string\n const urlObj = new URL(params.url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n urlObj.searchParams.forEach((value, key) => {\n allParams.push([key, value]);\n });\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {\n baseUrl: string;\n requestsPerSecond?: number;\n burstSize?: number;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n requestsPerSecond: config.requestsPerSecond,\n burstSize: config.burstSize,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","export interface TokenBucketOptions {\n /** Maximum requests per second. */\n requestsPerSecond: number;\n /** Maximum burst capacity. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Token bucket rate limiter for proactive request throttling.\n * Smooths request rate to stay within NetSuite's rate limits\n * instead of reacting to 429 errors.\n */\nexport class TokenBucket {\n private tokens: number;\n private readonly maxTokens: number;\n private readonly refillRate: number; // tokens per ms\n private lastRefill: number;\n\n constructor(options: TokenBucketOptions) {\n this.maxTokens = options.burstSize ?? options.requestsPerSecond;\n this.refillRate = options.requestsPerSecond / 1000;\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n }\n\n /**\n * Acquire a token, waiting if necessary.\n * Resolves immediately if a token is available, otherwise\n * delays until the bucket refills enough for one token.\n */\n async acquire(): Promise<void> {\n this.refill();\n\n if (this.tokens >= 1) {\n this.tokens--;\n return;\n }\n\n const waitMs = (1 - this.tokens) / this.refillRate;\n await new Promise<void>((resolve) => setTimeout(resolve, waitMs));\n this.refill();\n this.tokens--;\n }\n\n /** Number of tokens currently available. */\n get availableTokens(): number {\n this.refill();\n return this.tokens;\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefill;\n if (elapsed <= 0) return;\n\n this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);\n this.lastRefill = now;\n }\n}\n","import { RateLimiter } from './rate-limiter.js';\nimport { TokenBucket } from './token-bucket.js';\n\nexport interface ThrottlerOptions {\n /** Max concurrent requests. Default: 5. */\n concurrency?: number;\n /** Proactive rate limit (requests/sec). Undefined = disabled. */\n requestsPerSecond?: number;\n /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).\n * Token bucket fires first (delays to stay under rate limit), then the semaphore\n * limits parallelism.\n */\nexport class RequestThrottler {\n private readonly semaphore: RateLimiter;\n private readonly tokenBucket: TokenBucket | null;\n\n constructor(options: ThrottlerOptions = {}) {\n this.semaphore = new RateLimiter(options.concurrency ?? 5);\n this.tokenBucket =\n options.requestsPerSecond != null\n ? new TokenBucket({\n requestsPerSecond: options.requestsPerSecond,\n burstSize: options.burstSize,\n })\n : null;\n }\n\n /** Run an async function within both rate and concurrency limits. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n if (this.tokenBucket) {\n await this.tokenBucket.acquire();\n }\n return this.semaphore.run(fn);\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.semaphore.activeCount;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.semaphore.waitingCount;\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RequestThrottler } from './request-throttler.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly throttler: RequestThrottler;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.throttler = new RequestThrottler({\n concurrency: this.config.concurrency,\n requestsPerSecond: this.config.requestsPerSecond,\n burstSize: this.config.burstSize,\n });\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const shouldRetry = options.retry ?? options.method === 'GET';\n return this.throttler.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: shouldRetry ? this.config.maxRetries : 0 },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const timeoutMs = options.timeout ?? this.config.timeout;\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(timeoutMs);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n retry: true, // POST-for-query is idempotent\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Upsert a record via the REST Record API (create or update by externalId).\n * PUT /services/rest/record/v1/{recordType}/eid:{externalId}\n */\nexport async function upsertRecord(\n client: NetSuiteClient,\n recordType: string,\n externalId: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PUT',\n path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\nconst DEFAULT_TRANSFORM_TIMEOUT = 120_000; // 2 minutes\n\nexport interface TransformOptions {\n /** Request body (field overrides on the target record). */\n body?: Record<string, unknown>;\n /** Timeout in ms. Default: 120000 (2 minutes). */\n timeout?: number;\n}\n\nexport interface TransformResult {\n /** The internal ID of the newly created record. */\n id: string;\n /** The target record type. */\n type: string;\n}\n\n/**\n * Transform a record from one type to another via the NetSuite REST API.\n *\n * POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}\n *\n * Uses a longer default timeout (120s) since transforms can be slow.\n * Never retries — transforms are non-idempotent mutations.\n */\nexport async function transformRecord(\n client: NetSuiteClient,\n sourceType: string,\n sourceId: string | number,\n targetType: string,\n options?: TransformOptions,\n): Promise<TransformResult> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${sourceType}/${sourceId}/!transform/${targetType}`,\n body: options?.body ?? {},\n timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,\n retry: false,\n });\n\n // Extract ID from the Location header or response body\n const location = response.headers.get('Location');\n let id: string | undefined;\n\n if (location) {\n // Location: /services/rest/record/v1/{targetType}/{id}\n const match = location.match(/\\/(\\d+)$/);\n if (match) {\n id = match[1];\n }\n }\n\n // Fall back to response body if Location header doesn't have the ID\n if (!id && response.data) {\n const rawId = response.data['id'] ?? response.data['internalid'] ?? response.data['internalId'];\n if (rawId != null) {\n id = String(rawId);\n }\n }\n\n if (!id) {\n throw new Error(\n `Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. ` +\n `Location header: ${location ?? '(none)'}`,\n );\n }\n\n return { id, type: targetType };\n}\n"],"mappings":"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","names":["RECORD_BASE"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -16,6 +16,10 @@ interface NetSuiteConfig {
|
|
|
16
16
|
concurrency?: number;
|
|
17
17
|
/** Max retry attempts for retryable failures. Default: 3. */
|
|
18
18
|
maxRetries?: number;
|
|
19
|
+
/** Proactive rate limit in requests per second (token bucket). Undefined = disabled. */
|
|
20
|
+
requestsPerSecond?: number;
|
|
21
|
+
/** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
|
|
22
|
+
burstSize?: number;
|
|
19
23
|
/** Override the base URL (mainly for testing). */
|
|
20
24
|
baseUrl?: string;
|
|
21
25
|
}
|
|
@@ -58,6 +62,13 @@ interface RequestOptions {
|
|
|
58
62
|
body?: unknown;
|
|
59
63
|
headers?: Record<string, string>;
|
|
60
64
|
query?: Record<string, string>;
|
|
65
|
+
/** Override the global timeout for this request (in ms). */
|
|
66
|
+
timeout?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to retry on failure. Default: true for GET, false for mutations.
|
|
69
|
+
* Set to `true` for idempotent POST operations (e.g. SuiteQL queries).
|
|
70
|
+
*/
|
|
71
|
+
retry?: boolean;
|
|
61
72
|
}
|
|
62
73
|
/** Generic HTTP response wrapper. */
|
|
63
74
|
interface NetSuiteResponse<T = unknown> {
|
|
@@ -66,8 +77,10 @@ interface NetSuiteResponse<T = unknown> {
|
|
|
66
77
|
data: T;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {
|
|
80
|
+
interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {
|
|
70
81
|
baseUrl: string;
|
|
82
|
+
requestsPerSecond?: number;
|
|
83
|
+
burstSize?: number;
|
|
71
84
|
}
|
|
72
85
|
/** Validate config and fill defaults. */
|
|
73
86
|
declare function resolveConfig(config: NetSuiteConfig): ResolvedConfig;
|
|
@@ -86,7 +99,7 @@ declare function getRealm(accountId: string): string;
|
|
|
86
99
|
*/
|
|
87
100
|
declare class NetSuiteClient {
|
|
88
101
|
private readonly config;
|
|
89
|
-
private readonly
|
|
102
|
+
private readonly throttler;
|
|
90
103
|
constructor(config: NetSuiteConfig);
|
|
91
104
|
/** Make an authenticated request to the NetSuite REST API. */
|
|
92
105
|
request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>>;
|
|
@@ -167,12 +180,39 @@ declare function updateRecord(client: NetSuiteClient, recordType: string, id: st
|
|
|
167
180
|
* DELETE /services/rest/record/v1/{recordType}/{id}
|
|
168
181
|
*/
|
|
169
182
|
declare function deleteRecord(client: NetSuiteClient, recordType: string, id: string | number): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Upsert a record via the REST Record API (create or update by externalId).
|
|
185
|
+
* PUT /services/rest/record/v1/{recordType}/eid:{externalId}
|
|
186
|
+
*/
|
|
187
|
+
declare function upsertRecord(client: NetSuiteClient, recordType: string, externalId: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
170
188
|
/**
|
|
171
189
|
* Get a single record by ID via the REST Record API.
|
|
172
190
|
* GET /services/rest/record/v1/{recordType}/{id}
|
|
173
191
|
*/
|
|
174
192
|
declare function getRecord(client: NetSuiteClient, recordType: string, id: string | number): Promise<Record<string, unknown>>;
|
|
175
193
|
|
|
194
|
+
interface TransformOptions {
|
|
195
|
+
/** Request body (field overrides on the target record). */
|
|
196
|
+
body?: Record<string, unknown>;
|
|
197
|
+
/** Timeout in ms. Default: 120000 (2 minutes). */
|
|
198
|
+
timeout?: number;
|
|
199
|
+
}
|
|
200
|
+
interface TransformResult {
|
|
201
|
+
/** The internal ID of the newly created record. */
|
|
202
|
+
id: string;
|
|
203
|
+
/** The target record type. */
|
|
204
|
+
type: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Transform a record from one type to another via the NetSuite REST API.
|
|
208
|
+
*
|
|
209
|
+
* POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}
|
|
210
|
+
*
|
|
211
|
+
* Uses a longer default timeout (120s) since transforms can be slow.
|
|
212
|
+
* Never retries — transforms are non-idempotent mutations.
|
|
213
|
+
*/
|
|
214
|
+
declare function transformRecord(client: NetSuiteClient, sourceType: string, sourceId: string | number, targetType: string, options?: TransformOptions): Promise<TransformResult>;
|
|
215
|
+
|
|
176
216
|
/** Base error class for all NetSuite connector errors. */
|
|
177
217
|
declare class NetSuiteError extends Error {
|
|
178
218
|
readonly status?: number | undefined;
|
|
@@ -217,6 +257,59 @@ declare class RateLimiter {
|
|
|
217
257
|
get waitingCount(): number;
|
|
218
258
|
}
|
|
219
259
|
|
|
260
|
+
interface TokenBucketOptions {
|
|
261
|
+
/** Maximum requests per second. */
|
|
262
|
+
requestsPerSecond: number;
|
|
263
|
+
/** Maximum burst capacity. Defaults to requestsPerSecond. */
|
|
264
|
+
burstSize?: number;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Token bucket rate limiter for proactive request throttling.
|
|
268
|
+
* Smooths request rate to stay within NetSuite's rate limits
|
|
269
|
+
* instead of reacting to 429 errors.
|
|
270
|
+
*/
|
|
271
|
+
declare class TokenBucket {
|
|
272
|
+
private tokens;
|
|
273
|
+
private readonly maxTokens;
|
|
274
|
+
private readonly refillRate;
|
|
275
|
+
private lastRefill;
|
|
276
|
+
constructor(options: TokenBucketOptions);
|
|
277
|
+
/**
|
|
278
|
+
* Acquire a token, waiting if necessary.
|
|
279
|
+
* Resolves immediately if a token is available, otherwise
|
|
280
|
+
* delays until the bucket refills enough for one token.
|
|
281
|
+
*/
|
|
282
|
+
acquire(): Promise<void>;
|
|
283
|
+
/** Number of tokens currently available. */
|
|
284
|
+
get availableTokens(): number;
|
|
285
|
+
private refill;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface ThrottlerOptions {
|
|
289
|
+
/** Max concurrent requests. Default: 5. */
|
|
290
|
+
concurrency?: number;
|
|
291
|
+
/** Proactive rate limit (requests/sec). Undefined = disabled. */
|
|
292
|
+
requestsPerSecond?: number;
|
|
293
|
+
/** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
|
|
294
|
+
burstSize?: number;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).
|
|
298
|
+
* Token bucket fires first (delays to stay under rate limit), then the semaphore
|
|
299
|
+
* limits parallelism.
|
|
300
|
+
*/
|
|
301
|
+
declare class RequestThrottler {
|
|
302
|
+
private readonly semaphore;
|
|
303
|
+
private readonly tokenBucket;
|
|
304
|
+
constructor(options?: ThrottlerOptions);
|
|
305
|
+
/** Run an async function within both rate and concurrency limits. */
|
|
306
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
307
|
+
/** Number of currently active tasks. */
|
|
308
|
+
get activeCount(): number;
|
|
309
|
+
/** Number of tasks waiting for a slot. */
|
|
310
|
+
get waitingCount(): number;
|
|
311
|
+
}
|
|
312
|
+
|
|
220
313
|
interface RetryOptions {
|
|
221
314
|
maxRetries: number;
|
|
222
315
|
/** Base delay in ms before first retry. Default: 500. */
|
|
@@ -230,4 +323,4 @@ interface RetryOptions {
|
|
|
230
323
|
*/
|
|
231
324
|
declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
|
|
232
325
|
|
|
233
|
-
export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, TimeoutError, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, updateRecord, withRetry };
|
|
326
|
+
export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, RequestThrottler, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, type ThrottlerOptions, TimeoutError, TokenBucket, type TokenBucketOptions, type TransformOptions, type TransformResult, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, transformRecord, updateRecord, upsertRecord, withRetry };
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ interface NetSuiteConfig {
|
|
|
16
16
|
concurrency?: number;
|
|
17
17
|
/** Max retry attempts for retryable failures. Default: 3. */
|
|
18
18
|
maxRetries?: number;
|
|
19
|
+
/** Proactive rate limit in requests per second (token bucket). Undefined = disabled. */
|
|
20
|
+
requestsPerSecond?: number;
|
|
21
|
+
/** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
|
|
22
|
+
burstSize?: number;
|
|
19
23
|
/** Override the base URL (mainly for testing). */
|
|
20
24
|
baseUrl?: string;
|
|
21
25
|
}
|
|
@@ -58,6 +62,13 @@ interface RequestOptions {
|
|
|
58
62
|
body?: unknown;
|
|
59
63
|
headers?: Record<string, string>;
|
|
60
64
|
query?: Record<string, string>;
|
|
65
|
+
/** Override the global timeout for this request (in ms). */
|
|
66
|
+
timeout?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to retry on failure. Default: true for GET, false for mutations.
|
|
69
|
+
* Set to `true` for idempotent POST operations (e.g. SuiteQL queries).
|
|
70
|
+
*/
|
|
71
|
+
retry?: boolean;
|
|
61
72
|
}
|
|
62
73
|
/** Generic HTTP response wrapper. */
|
|
63
74
|
interface NetSuiteResponse<T = unknown> {
|
|
@@ -66,8 +77,10 @@ interface NetSuiteResponse<T = unknown> {
|
|
|
66
77
|
data: T;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {
|
|
80
|
+
interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {
|
|
70
81
|
baseUrl: string;
|
|
82
|
+
requestsPerSecond?: number;
|
|
83
|
+
burstSize?: number;
|
|
71
84
|
}
|
|
72
85
|
/** Validate config and fill defaults. */
|
|
73
86
|
declare function resolveConfig(config: NetSuiteConfig): ResolvedConfig;
|
|
@@ -86,7 +99,7 @@ declare function getRealm(accountId: string): string;
|
|
|
86
99
|
*/
|
|
87
100
|
declare class NetSuiteClient {
|
|
88
101
|
private readonly config;
|
|
89
|
-
private readonly
|
|
102
|
+
private readonly throttler;
|
|
90
103
|
constructor(config: NetSuiteConfig);
|
|
91
104
|
/** Make an authenticated request to the NetSuite REST API. */
|
|
92
105
|
request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>>;
|
|
@@ -167,12 +180,39 @@ declare function updateRecord(client: NetSuiteClient, recordType: string, id: st
|
|
|
167
180
|
* DELETE /services/rest/record/v1/{recordType}/{id}
|
|
168
181
|
*/
|
|
169
182
|
declare function deleteRecord(client: NetSuiteClient, recordType: string, id: string | number): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Upsert a record via the REST Record API (create or update by externalId).
|
|
185
|
+
* PUT /services/rest/record/v1/{recordType}/eid:{externalId}
|
|
186
|
+
*/
|
|
187
|
+
declare function upsertRecord(client: NetSuiteClient, recordType: string, externalId: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
170
188
|
/**
|
|
171
189
|
* Get a single record by ID via the REST Record API.
|
|
172
190
|
* GET /services/rest/record/v1/{recordType}/{id}
|
|
173
191
|
*/
|
|
174
192
|
declare function getRecord(client: NetSuiteClient, recordType: string, id: string | number): Promise<Record<string, unknown>>;
|
|
175
193
|
|
|
194
|
+
interface TransformOptions {
|
|
195
|
+
/** Request body (field overrides on the target record). */
|
|
196
|
+
body?: Record<string, unknown>;
|
|
197
|
+
/** Timeout in ms. Default: 120000 (2 minutes). */
|
|
198
|
+
timeout?: number;
|
|
199
|
+
}
|
|
200
|
+
interface TransformResult {
|
|
201
|
+
/** The internal ID of the newly created record. */
|
|
202
|
+
id: string;
|
|
203
|
+
/** The target record type. */
|
|
204
|
+
type: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Transform a record from one type to another via the NetSuite REST API.
|
|
208
|
+
*
|
|
209
|
+
* POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}
|
|
210
|
+
*
|
|
211
|
+
* Uses a longer default timeout (120s) since transforms can be slow.
|
|
212
|
+
* Never retries — transforms are non-idempotent mutations.
|
|
213
|
+
*/
|
|
214
|
+
declare function transformRecord(client: NetSuiteClient, sourceType: string, sourceId: string | number, targetType: string, options?: TransformOptions): Promise<TransformResult>;
|
|
215
|
+
|
|
176
216
|
/** Base error class for all NetSuite connector errors. */
|
|
177
217
|
declare class NetSuiteError extends Error {
|
|
178
218
|
readonly status?: number | undefined;
|
|
@@ -217,6 +257,59 @@ declare class RateLimiter {
|
|
|
217
257
|
get waitingCount(): number;
|
|
218
258
|
}
|
|
219
259
|
|
|
260
|
+
interface TokenBucketOptions {
|
|
261
|
+
/** Maximum requests per second. */
|
|
262
|
+
requestsPerSecond: number;
|
|
263
|
+
/** Maximum burst capacity. Defaults to requestsPerSecond. */
|
|
264
|
+
burstSize?: number;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Token bucket rate limiter for proactive request throttling.
|
|
268
|
+
* Smooths request rate to stay within NetSuite's rate limits
|
|
269
|
+
* instead of reacting to 429 errors.
|
|
270
|
+
*/
|
|
271
|
+
declare class TokenBucket {
|
|
272
|
+
private tokens;
|
|
273
|
+
private readonly maxTokens;
|
|
274
|
+
private readonly refillRate;
|
|
275
|
+
private lastRefill;
|
|
276
|
+
constructor(options: TokenBucketOptions);
|
|
277
|
+
/**
|
|
278
|
+
* Acquire a token, waiting if necessary.
|
|
279
|
+
* Resolves immediately if a token is available, otherwise
|
|
280
|
+
* delays until the bucket refills enough for one token.
|
|
281
|
+
*/
|
|
282
|
+
acquire(): Promise<void>;
|
|
283
|
+
/** Number of tokens currently available. */
|
|
284
|
+
get availableTokens(): number;
|
|
285
|
+
private refill;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface ThrottlerOptions {
|
|
289
|
+
/** Max concurrent requests. Default: 5. */
|
|
290
|
+
concurrency?: number;
|
|
291
|
+
/** Proactive rate limit (requests/sec). Undefined = disabled. */
|
|
292
|
+
requestsPerSecond?: number;
|
|
293
|
+
/** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
|
|
294
|
+
burstSize?: number;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).
|
|
298
|
+
* Token bucket fires first (delays to stay under rate limit), then the semaphore
|
|
299
|
+
* limits parallelism.
|
|
300
|
+
*/
|
|
301
|
+
declare class RequestThrottler {
|
|
302
|
+
private readonly semaphore;
|
|
303
|
+
private readonly tokenBucket;
|
|
304
|
+
constructor(options?: ThrottlerOptions);
|
|
305
|
+
/** Run an async function within both rate and concurrency limits. */
|
|
306
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
307
|
+
/** Number of currently active tasks. */
|
|
308
|
+
get activeCount(): number;
|
|
309
|
+
/** Number of tasks waiting for a slot. */
|
|
310
|
+
get waitingCount(): number;
|
|
311
|
+
}
|
|
312
|
+
|
|
220
313
|
interface RetryOptions {
|
|
221
314
|
maxRetries: number;
|
|
222
315
|
/** Base delay in ms before first retry. Default: 500. */
|
|
@@ -230,4 +323,4 @@ interface RetryOptions {
|
|
|
230
323
|
*/
|
|
231
324
|
declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
|
|
232
325
|
|
|
233
|
-
export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, TimeoutError, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, updateRecord, withRetry };
|
|
326
|
+
export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, RequestThrottler, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, type ThrottlerOptions, TimeoutError, TokenBucket, type TokenBucketOptions, type TransformOptions, type TransformResult, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, transformRecord, updateRecord, upsertRecord, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,8 @@ function resolveConfig(config) {
|
|
|
89
89
|
timeout: config.timeout ?? DEFAULTS.timeout,
|
|
90
90
|
concurrency: config.concurrency ?? DEFAULTS.concurrency,
|
|
91
91
|
maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,
|
|
92
|
+
requestsPerSecond: config.requestsPerSecond,
|
|
93
|
+
burstSize: config.burstSize,
|
|
92
94
|
baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId)
|
|
93
95
|
};
|
|
94
96
|
}
|
|
@@ -181,6 +183,77 @@ var RateLimiter = class {
|
|
|
181
183
|
}
|
|
182
184
|
};
|
|
183
185
|
|
|
186
|
+
// src/http/token-bucket.ts
|
|
187
|
+
var TokenBucket = class {
|
|
188
|
+
tokens;
|
|
189
|
+
maxTokens;
|
|
190
|
+
refillRate;
|
|
191
|
+
// tokens per ms
|
|
192
|
+
lastRefill;
|
|
193
|
+
constructor(options) {
|
|
194
|
+
this.maxTokens = options.burstSize ?? options.requestsPerSecond;
|
|
195
|
+
this.refillRate = options.requestsPerSecond / 1e3;
|
|
196
|
+
this.tokens = this.maxTokens;
|
|
197
|
+
this.lastRefill = Date.now();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Acquire a token, waiting if necessary.
|
|
201
|
+
* Resolves immediately if a token is available, otherwise
|
|
202
|
+
* delays until the bucket refills enough for one token.
|
|
203
|
+
*/
|
|
204
|
+
async acquire() {
|
|
205
|
+
this.refill();
|
|
206
|
+
if (this.tokens >= 1) {
|
|
207
|
+
this.tokens--;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const waitMs = (1 - this.tokens) / this.refillRate;
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
212
|
+
this.refill();
|
|
213
|
+
this.tokens--;
|
|
214
|
+
}
|
|
215
|
+
/** Number of tokens currently available. */
|
|
216
|
+
get availableTokens() {
|
|
217
|
+
this.refill();
|
|
218
|
+
return this.tokens;
|
|
219
|
+
}
|
|
220
|
+
refill() {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const elapsed = now - this.lastRefill;
|
|
223
|
+
if (elapsed <= 0) return;
|
|
224
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
225
|
+
this.lastRefill = now;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/http/request-throttler.ts
|
|
230
|
+
var RequestThrottler = class {
|
|
231
|
+
semaphore;
|
|
232
|
+
tokenBucket;
|
|
233
|
+
constructor(options = {}) {
|
|
234
|
+
this.semaphore = new RateLimiter(options.concurrency ?? 5);
|
|
235
|
+
this.tokenBucket = options.requestsPerSecond != null ? new TokenBucket({
|
|
236
|
+
requestsPerSecond: options.requestsPerSecond,
|
|
237
|
+
burstSize: options.burstSize
|
|
238
|
+
}) : null;
|
|
239
|
+
}
|
|
240
|
+
/** Run an async function within both rate and concurrency limits. */
|
|
241
|
+
async run(fn) {
|
|
242
|
+
if (this.tokenBucket) {
|
|
243
|
+
await this.tokenBucket.acquire();
|
|
244
|
+
}
|
|
245
|
+
return this.semaphore.run(fn);
|
|
246
|
+
}
|
|
247
|
+
/** Number of currently active tasks. */
|
|
248
|
+
get activeCount() {
|
|
249
|
+
return this.semaphore.activeCount;
|
|
250
|
+
}
|
|
251
|
+
/** Number of tasks waiting for a slot. */
|
|
252
|
+
get waitingCount() {
|
|
253
|
+
return this.semaphore.waitingCount;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
184
257
|
// src/http/retry.ts
|
|
185
258
|
async function withRetry(fn, options) {
|
|
186
259
|
const { maxRetries, baseDelay = 500, maxDelay = 3e4 } = options;
|
|
@@ -216,17 +289,22 @@ function sleep(ms) {
|
|
|
216
289
|
// src/http/client.ts
|
|
217
290
|
var NetSuiteClient = class {
|
|
218
291
|
config;
|
|
219
|
-
|
|
292
|
+
throttler;
|
|
220
293
|
constructor(config) {
|
|
221
294
|
this.config = resolveConfig(config);
|
|
222
|
-
this.
|
|
295
|
+
this.throttler = new RequestThrottler({
|
|
296
|
+
concurrency: this.config.concurrency,
|
|
297
|
+
requestsPerSecond: this.config.requestsPerSecond,
|
|
298
|
+
burstSize: this.config.burstSize
|
|
299
|
+
});
|
|
223
300
|
}
|
|
224
301
|
/** Make an authenticated request to the NetSuite REST API. */
|
|
225
302
|
async request(options) {
|
|
226
|
-
|
|
303
|
+
const shouldRetry = options.retry ?? options.method === "GET";
|
|
304
|
+
return this.throttler.run(
|
|
227
305
|
() => withRetry(
|
|
228
306
|
() => this.executeRequest(options),
|
|
229
|
-
{ maxRetries: this.config.maxRetries }
|
|
307
|
+
{ maxRetries: shouldRetry ? this.config.maxRetries : 0 }
|
|
230
308
|
)
|
|
231
309
|
);
|
|
232
310
|
}
|
|
@@ -256,8 +334,9 @@ var NetSuiteClient = class {
|
|
|
256
334
|
Accept: "application/json",
|
|
257
335
|
...options.headers
|
|
258
336
|
};
|
|
337
|
+
const timeoutMs = options.timeout ?? this.config.timeout;
|
|
259
338
|
const controller = new AbortController();
|
|
260
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
339
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
261
340
|
try {
|
|
262
341
|
const response = await fetch(url, {
|
|
263
342
|
method: options.method,
|
|
@@ -275,7 +354,7 @@ var NetSuiteClient = class {
|
|
|
275
354
|
clearTimeout(timeoutId);
|
|
276
355
|
if (error instanceof NetSuiteError) throw error;
|
|
277
356
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
278
|
-
throw new TimeoutError(
|
|
357
|
+
throw new TimeoutError(timeoutMs);
|
|
279
358
|
}
|
|
280
359
|
throw new NetSuiteError(
|
|
281
360
|
`Network error: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -336,7 +415,9 @@ async function executeSuiteQL(client, query, options) {
|
|
|
336
415
|
query: {
|
|
337
416
|
limit: limit.toString(),
|
|
338
417
|
offset: offset.toString()
|
|
339
|
-
}
|
|
418
|
+
},
|
|
419
|
+
retry: true
|
|
420
|
+
// POST-for-query is idempotent
|
|
340
421
|
});
|
|
341
422
|
const items = (response.data.items ?? []).map(({ links, ...rest }) => rest);
|
|
342
423
|
return {
|
|
@@ -388,6 +469,14 @@ async function deleteRecord(client, recordType, id) {
|
|
|
388
469
|
path: `${RECORD_BASE}/${recordType}/${id}`
|
|
389
470
|
});
|
|
390
471
|
}
|
|
472
|
+
async function upsertRecord(client, recordType, externalId, data) {
|
|
473
|
+
const response = await client.request({
|
|
474
|
+
method: "PUT",
|
|
475
|
+
path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,
|
|
476
|
+
body: data
|
|
477
|
+
});
|
|
478
|
+
return response.data;
|
|
479
|
+
}
|
|
391
480
|
async function getRecord(client, recordType, id) {
|
|
392
481
|
const response = await client.request({
|
|
393
482
|
method: "GET",
|
|
@@ -395,13 +484,48 @@ async function getRecord(client, recordType, id) {
|
|
|
395
484
|
});
|
|
396
485
|
return response.data;
|
|
397
486
|
}
|
|
487
|
+
|
|
488
|
+
// src/rest/transform.ts
|
|
489
|
+
var RECORD_BASE2 = "/services/rest/record/v1";
|
|
490
|
+
var DEFAULT_TRANSFORM_TIMEOUT = 12e4;
|
|
491
|
+
async function transformRecord(client, sourceType, sourceId, targetType, options) {
|
|
492
|
+
const response = await client.request({
|
|
493
|
+
method: "POST",
|
|
494
|
+
path: `${RECORD_BASE2}/${sourceType}/${sourceId}/!transform/${targetType}`,
|
|
495
|
+
body: options?.body ?? {},
|
|
496
|
+
timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,
|
|
497
|
+
retry: false
|
|
498
|
+
});
|
|
499
|
+
const location = response.headers.get("Location");
|
|
500
|
+
let id;
|
|
501
|
+
if (location) {
|
|
502
|
+
const match = location.match(/\/(\d+)$/);
|
|
503
|
+
if (match) {
|
|
504
|
+
id = match[1];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!id && response.data) {
|
|
508
|
+
const rawId = response.data["id"] ?? response.data["internalid"] ?? response.data["internalId"];
|
|
509
|
+
if (rawId != null) {
|
|
510
|
+
id = String(rawId);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!id) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. Location header: ${location ?? "(none)"}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return { id, type: targetType };
|
|
519
|
+
}
|
|
398
520
|
export {
|
|
399
521
|
AuthError,
|
|
400
522
|
NetSuiteClient,
|
|
401
523
|
NetSuiteError,
|
|
402
524
|
RateLimitError,
|
|
403
525
|
RateLimiter,
|
|
526
|
+
RequestThrottler,
|
|
404
527
|
TimeoutError,
|
|
528
|
+
TokenBucket,
|
|
405
529
|
buildAuthorizationHeader,
|
|
406
530
|
buildSignatureBaseString,
|
|
407
531
|
createRecord,
|
|
@@ -418,7 +542,9 @@ export {
|
|
|
418
542
|
percentEncode,
|
|
419
543
|
resolveConfig,
|
|
420
544
|
signHmacSha256,
|
|
545
|
+
transformRecord,
|
|
421
546
|
updateRecord,
|
|
547
|
+
upsertRecord,
|
|
422
548
|
withRetry
|
|
423
549
|
};
|
|
424
550
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts"],"sourcesContent":["import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value\n const sorted = [...params].sort((a, b) => {\n const keyCompare = a[0]!.localeCompare(b[0]!);\n return keyCompare !== 0 ? keyCompare : a[1]!.localeCompare(b[1]!);\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string\n const urlObj = new URL(params.url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n urlObj.searchParams.forEach((value, key) => {\n allParams.push([key, value]);\n });\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {\n baseUrl: string;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RateLimiter } from './rate-limiter.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly rateLimiter: RateLimiter;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.rateLimiter = new RateLimiter(this.config.concurrency);\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n return this.rateLimiter.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: this.config.maxRetries },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(this.config.timeout);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/token-bucket.ts","../src/http/request-throttler.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts","../src/rest/transform.ts"],"sourcesContent":["import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value\n const sorted = [...params].sort((a, b) => {\n const keyCompare = a[0]!.localeCompare(b[0]!);\n return keyCompare !== 0 ? keyCompare : a[1]!.localeCompare(b[1]!);\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string\n const urlObj = new URL(params.url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n urlObj.searchParams.forEach((value, key) => {\n allParams.push([key, value]);\n });\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {\n baseUrl: string;\n requestsPerSecond?: number;\n burstSize?: number;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n requestsPerSecond: config.requestsPerSecond,\n burstSize: config.burstSize,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","export interface TokenBucketOptions {\n /** Maximum requests per second. */\n requestsPerSecond: number;\n /** Maximum burst capacity. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Token bucket rate limiter for proactive request throttling.\n * Smooths request rate to stay within NetSuite's rate limits\n * instead of reacting to 429 errors.\n */\nexport class TokenBucket {\n private tokens: number;\n private readonly maxTokens: number;\n private readonly refillRate: number; // tokens per ms\n private lastRefill: number;\n\n constructor(options: TokenBucketOptions) {\n this.maxTokens = options.burstSize ?? options.requestsPerSecond;\n this.refillRate = options.requestsPerSecond / 1000;\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n }\n\n /**\n * Acquire a token, waiting if necessary.\n * Resolves immediately if a token is available, otherwise\n * delays until the bucket refills enough for one token.\n */\n async acquire(): Promise<void> {\n this.refill();\n\n if (this.tokens >= 1) {\n this.tokens--;\n return;\n }\n\n const waitMs = (1 - this.tokens) / this.refillRate;\n await new Promise<void>((resolve) => setTimeout(resolve, waitMs));\n this.refill();\n this.tokens--;\n }\n\n /** Number of tokens currently available. */\n get availableTokens(): number {\n this.refill();\n return this.tokens;\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefill;\n if (elapsed <= 0) return;\n\n this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);\n this.lastRefill = now;\n }\n}\n","import { RateLimiter } from './rate-limiter.js';\nimport { TokenBucket } from './token-bucket.js';\n\nexport interface ThrottlerOptions {\n /** Max concurrent requests. Default: 5. */\n concurrency?: number;\n /** Proactive rate limit (requests/sec). Undefined = disabled. */\n requestsPerSecond?: number;\n /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).\n * Token bucket fires first (delays to stay under rate limit), then the semaphore\n * limits parallelism.\n */\nexport class RequestThrottler {\n private readonly semaphore: RateLimiter;\n private readonly tokenBucket: TokenBucket | null;\n\n constructor(options: ThrottlerOptions = {}) {\n this.semaphore = new RateLimiter(options.concurrency ?? 5);\n this.tokenBucket =\n options.requestsPerSecond != null\n ? new TokenBucket({\n requestsPerSecond: options.requestsPerSecond,\n burstSize: options.burstSize,\n })\n : null;\n }\n\n /** Run an async function within both rate and concurrency limits. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n if (this.tokenBucket) {\n await this.tokenBucket.acquire();\n }\n return this.semaphore.run(fn);\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.semaphore.activeCount;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.semaphore.waitingCount;\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RequestThrottler } from './request-throttler.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly throttler: RequestThrottler;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.throttler = new RequestThrottler({\n concurrency: this.config.concurrency,\n requestsPerSecond: this.config.requestsPerSecond,\n burstSize: this.config.burstSize,\n });\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const shouldRetry = options.retry ?? options.method === 'GET';\n return this.throttler.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: shouldRetry ? this.config.maxRetries : 0 },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const timeoutMs = options.timeout ?? this.config.timeout;\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(timeoutMs);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n retry: true, // POST-for-query is idempotent\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Upsert a record via the REST Record API (create or update by externalId).\n * PUT /services/rest/record/v1/{recordType}/eid:{externalId}\n */\nexport async function upsertRecord(\n client: NetSuiteClient,\n recordType: string,\n externalId: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PUT',\n path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\nconst DEFAULT_TRANSFORM_TIMEOUT = 120_000; // 2 minutes\n\nexport interface TransformOptions {\n /** Request body (field overrides on the target record). */\n body?: Record<string, unknown>;\n /** Timeout in ms. Default: 120000 (2 minutes). */\n timeout?: number;\n}\n\nexport interface TransformResult {\n /** The internal ID of the newly created record. */\n id: string;\n /** The target record type. */\n type: string;\n}\n\n/**\n * Transform a record from one type to another via the NetSuite REST API.\n *\n * POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}\n *\n * Uses a longer default timeout (120s) since transforms can be slow.\n * Never retries — transforms are non-idempotent mutations.\n */\nexport async function transformRecord(\n client: NetSuiteClient,\n sourceType: string,\n sourceId: string | number,\n targetType: string,\n options?: TransformOptions,\n): Promise<TransformResult> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${sourceType}/${sourceId}/!transform/${targetType}`,\n body: options?.body ?? {},\n timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,\n retry: false,\n });\n\n // Extract ID from the Location header or response body\n const location = response.headers.get('Location');\n let id: string | undefined;\n\n if (location) {\n // Location: /services/rest/record/v1/{targetType}/{id}\n const match = location.match(/\\/(\\d+)$/);\n if (match) {\n id = match[1];\n }\n }\n\n // Fall back to response body if Location header doesn't have the ID\n if (!id && response.data) {\n const rawId = response.data['id'] ?? response.data['internalid'] ?? response.data['internalId'];\n if (rawId != null) {\n id = String(rawId);\n }\n }\n\n if (!id) {\n throw new Error(\n `Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. ` +\n `Location header: ${location ?? '(none)'}`,\n );\n }\n\n return { id, type: targetType };\n}\n"],"mappings":"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","names":["RECORD_BASE"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suiteportal/connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "NetSuite OAuth 1.0a connector with SuiteQL execution and REST Record CRUD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"author": "Trey Hulse",
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/
|
|
27
|
+
"url": "https://github.com/Suite-Portal/netsuite-orm",
|
|
28
28
|
"directory": "packages/connector"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|