quickbase-js 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -1107
- package/dist/auth/ticket.d.ts.map +1 -1
- package/dist/auth/ticket.js +4 -3
- package/dist/auth/ticket.js.map +1 -1
- package/dist/client/request.d.ts +4 -0
- package/dist/client/request.d.ts.map +1 -1
- package/dist/client/request.js +15 -3
- package/dist/client/request.js.map +1 -1
- package/dist/generated/client.d.ts +2 -0
- package/dist/generated/client.d.ts.map +1 -1
- package/dist/generated/client.js +134 -113
- package/dist/generated/client.js.map +1 -1
- package/dist/generated/types.d.ts +2700 -2256
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/generated/types.js +0 -2
- package/dist/generated/types.js.map +1 -1
- package/dist/quickbase.esm.js +3 -3
- package/dist/quickbase.esm.js.map +3 -3
- package/dist/quickbase.min.js +3 -3
- package/dist/quickbase.min.js.map +3 -3
- package/package.json +16 -13
- package/src/auth/index.ts +55 -0
- package/src/auth/sso-token.ts +143 -0
- package/src/auth/temp-token.ts +160 -0
- package/src/auth/ticket.ts +329 -0
- package/src/auth/token-cache.ts +97 -0
- package/src/auth/types.ts +50 -0
- package/src/auth/user-token.ts +38 -0
- package/src/cli/schema.ts +512 -0
- package/src/client/index.ts +36 -0
- package/src/client/pagination.ts +337 -0
- package/src/client/request.ts +544 -0
- package/src/client/retry.ts +169 -0
- package/src/client/throttle.ts +105 -0
- package/src/core/config.ts +160 -0
- package/src/core/dates.ts +63 -0
- package/src/core/errors.ts +349 -0
- package/src/core/index.ts +50 -0
- package/src/core/logger.ts +113 -0
- package/src/core/schema.ts +213 -0
- package/src/core/transform.ts +295 -0
- package/src/core/types.ts +337 -0
- package/src/generated/client.ts +1785 -0
- package/src/generated/operations.json +672 -0
- package/src/generated/types.ts +4884 -0
- package/src/index.ts +259 -0
- package/src/xml/codepages.ts +119 -0
- package/src/xml/dbvars.ts +75 -0
- package/src/xml/errors.ts +133 -0
- package/src/xml/fields.ts +121 -0
- package/src/xml/groups.ts +560 -0
- package/src/xml/html.ts +152 -0
- package/src/xml/index.ts +1196 -0
- package/src/xml/metadata.ts +111 -0
- package/src/xml/records.ts +301 -0
- package/src/xml/request.ts +238 -0
- package/src/xml/roles.ts +321 -0
- package/src/xml/schema.ts +436 -0
- package/src/xml/transform.ts +381 -0
- package/src/xml/types.ts +602 -0
- package/src/xml/users.ts +221 -0
- package/src/xml/webhooks.ts +260 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side throttling (optional)
|
|
3
|
+
*
|
|
4
|
+
* Implements a sliding window rate limiter that matches QuickBase's
|
|
5
|
+
* "100 requests per 10 seconds per user token" limit.
|
|
6
|
+
*
|
|
7
|
+
* This is OPTIONAL - QuickBase handles rate limiting server-side.
|
|
8
|
+
* Enable this if you want to avoid hitting 429s proactively.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class SlidingWindowThrottle {
|
|
12
|
+
private readonly requestsPer10Seconds: number;
|
|
13
|
+
private readonly timestamps: number[] = [];
|
|
14
|
+
|
|
15
|
+
constructor(requestsPer10Seconds: number = 100) {
|
|
16
|
+
this.requestsPer10Seconds = requestsPer10Seconds;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wait until a request slot is available
|
|
21
|
+
*/
|
|
22
|
+
async acquire(): Promise<void> {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const windowStart = now - 10000; // 10 second window
|
|
25
|
+
|
|
26
|
+
// Remove timestamps outside the window
|
|
27
|
+
while (this.timestamps.length > 0 && this.timestamps[0]! < windowStart) {
|
|
28
|
+
this.timestamps.shift();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If we're at the limit, wait until the oldest request exits the window
|
|
32
|
+
if (this.timestamps.length >= this.requestsPer10Seconds) {
|
|
33
|
+
const oldestTimestamp = this.timestamps[0]!;
|
|
34
|
+
const waitTime = oldestTimestamp + 10000 - now;
|
|
35
|
+
|
|
36
|
+
if (waitTime > 0) {
|
|
37
|
+
await this.sleep(waitTime);
|
|
38
|
+
// Recurse to recheck after waiting
|
|
39
|
+
return this.acquire();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Record this request
|
|
44
|
+
this.timestamps.push(Date.now());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the number of requests made in the current window
|
|
49
|
+
*/
|
|
50
|
+
getWindowCount(): number {
|
|
51
|
+
const windowStart = Date.now() - 10000;
|
|
52
|
+
return this.timestamps.filter((t) => t >= windowStart).length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get remaining requests available in the current window
|
|
57
|
+
*/
|
|
58
|
+
getRemaining(): number {
|
|
59
|
+
return Math.max(0, this.requestsPer10Seconds - this.getWindowCount());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reset the throttle state
|
|
64
|
+
*/
|
|
65
|
+
reset(): void {
|
|
66
|
+
this.timestamps.length = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private sleep(ms: number): Promise<void> {
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* No-op throttle for when proactive throttling is disabled
|
|
76
|
+
*/
|
|
77
|
+
export class NoOpThrottle {
|
|
78
|
+
async acquire(): Promise<void> {
|
|
79
|
+
// No-op
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getWindowCount(): number {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getRemaining(): number {
|
|
87
|
+
return Infinity;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
reset(): void {
|
|
91
|
+
// No-op
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type Throttle = SlidingWindowThrottle | NoOpThrottle;
|
|
96
|
+
|
|
97
|
+
export function createThrottle(config: {
|
|
98
|
+
enabled: boolean;
|
|
99
|
+
requestsPer10Seconds: number;
|
|
100
|
+
}): Throttle {
|
|
101
|
+
if (config.enabled) {
|
|
102
|
+
return new SlidingWindowThrottle(config.requestsPer10Seconds);
|
|
103
|
+
}
|
|
104
|
+
return new NoOpThrottle();
|
|
105
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration resolution and validation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { QuickbaseConfig, ResolvedConfig, RetryConfig } from './types.js';
|
|
6
|
+
import { DEFAULT_CONFIG } from './types.js';
|
|
7
|
+
import { resolveSchema } from './schema.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve user configuration with defaults
|
|
11
|
+
*/
|
|
12
|
+
export function resolveConfig(config: QuickbaseConfig): ResolvedConfig {
|
|
13
|
+
validateConfig(config);
|
|
14
|
+
|
|
15
|
+
const retry: Required<RetryConfig> = {
|
|
16
|
+
maxAttempts: config.rateLimit?.retry?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
17
|
+
initialDelayMs: config.rateLimit?.retry?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
|
|
18
|
+
maxDelayMs: config.rateLimit?.retry?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
|
|
19
|
+
multiplier: config.rateLimit?.retry?.multiplier ?? DEFAULT_CONFIG.retry.multiplier,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const proactiveThrottle = {
|
|
23
|
+
enabled: config.rateLimit?.proactiveThrottle?.enabled ?? DEFAULT_CONFIG.proactiveThrottle.enabled,
|
|
24
|
+
requestsPer10Seconds:
|
|
25
|
+
config.rateLimit?.proactiveThrottle?.requestsPer10Seconds ??
|
|
26
|
+
DEFAULT_CONFIG.proactiveThrottle.requestsPer10Seconds,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
realm: config.realm,
|
|
31
|
+
baseUrl: config.baseUrl ?? DEFAULT_CONFIG.baseUrl,
|
|
32
|
+
timeout: config.timeout ?? DEFAULT_CONFIG.timeout,
|
|
33
|
+
debug: config.debug ?? DEFAULT_CONFIG.debug,
|
|
34
|
+
fetchApi: config.fetchApi ?? globalThis.fetch.bind(globalThis),
|
|
35
|
+
auth: config.auth,
|
|
36
|
+
retry,
|
|
37
|
+
proactiveThrottle,
|
|
38
|
+
onRateLimit: config.rateLimit?.onRateLimit,
|
|
39
|
+
autoPaginate: config.autoPaginate ?? DEFAULT_CONFIG.autoPaginate,
|
|
40
|
+
convertDates: config.convertDates ?? DEFAULT_CONFIG.convertDates,
|
|
41
|
+
schema: resolveSchema(config.schema),
|
|
42
|
+
readOnly: config.readOnly ?? false,
|
|
43
|
+
appToken: config.appToken,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate configuration and throw if invalid
|
|
49
|
+
*/
|
|
50
|
+
function validateConfig(config: QuickbaseConfig): void {
|
|
51
|
+
if (!config.realm) {
|
|
52
|
+
throw new Error('QuickBase realm is required');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof config.realm !== 'string') {
|
|
56
|
+
throw new Error('QuickBase realm must be a string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove .quickbase.com suffix if provided
|
|
60
|
+
if (config.realm.includes('.')) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'Realm should be just the subdomain (e.g., "mycompany" not "mycompany.quickbase.com")'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!config.auth) {
|
|
67
|
+
throw new Error('Authentication configuration is required');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
validateAuthConfig(config.auth);
|
|
71
|
+
|
|
72
|
+
if (config.timeout !== undefined) {
|
|
73
|
+
if (typeof config.timeout !== 'number' || config.timeout <= 0) {
|
|
74
|
+
throw new Error('Timeout must be a positive number');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (config.rateLimit?.retry) {
|
|
79
|
+
validateRetryConfig(config.rateLimit.retry);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (config.rateLimit?.proactiveThrottle) {
|
|
83
|
+
validateThrottleConfig(config.rateLimit.proactiveThrottle);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateAuthConfig(auth: QuickbaseConfig['auth']): void {
|
|
88
|
+
switch (auth.type) {
|
|
89
|
+
case 'user-token':
|
|
90
|
+
if (!auth.userToken) {
|
|
91
|
+
throw new Error('User token is required for user-token auth');
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case 'temp-token':
|
|
96
|
+
// userToken is optional for temp-token auth
|
|
97
|
+
// When running in Code Pages, the browser session is used
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case 'sso':
|
|
101
|
+
if (!auth.samlToken) {
|
|
102
|
+
throw new Error('SAML token is required for SSO auth');
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
// XML-API-TICKET: Remove this case if XML API is discontinued
|
|
107
|
+
case 'ticket':
|
|
108
|
+
if (!auth.username) {
|
|
109
|
+
throw new Error('Username is required for ticket auth');
|
|
110
|
+
}
|
|
111
|
+
if (!auth.password) {
|
|
112
|
+
throw new Error('Password is required for ticket auth');
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
throw new Error(`Unknown auth type: ${(auth as { type: string }).type}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateRetryConfig(retry: RetryConfig): void {
|
|
122
|
+
if (retry.maxAttempts !== undefined) {
|
|
123
|
+
if (typeof retry.maxAttempts !== 'number' || retry.maxAttempts < 0) {
|
|
124
|
+
throw new Error('maxAttempts must be a non-negative number');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (retry.initialDelayMs !== undefined) {
|
|
129
|
+
if (typeof retry.initialDelayMs !== 'number' || retry.initialDelayMs < 0) {
|
|
130
|
+
throw new Error('initialDelayMs must be a non-negative number');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (retry.maxDelayMs !== undefined) {
|
|
135
|
+
if (typeof retry.maxDelayMs !== 'number' || retry.maxDelayMs < 0) {
|
|
136
|
+
throw new Error('maxDelayMs must be a non-negative number');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (retry.multiplier !== undefined) {
|
|
141
|
+
if (typeof retry.multiplier !== 'number' || retry.multiplier < 1) {
|
|
142
|
+
throw new Error('multiplier must be a number >= 1');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateThrottleConfig(throttle: { enabled?: boolean; requestsPer10Seconds?: number }): void {
|
|
148
|
+
if (throttle.requestsPer10Seconds !== undefined) {
|
|
149
|
+
if (typeof throttle.requestsPer10Seconds !== 'number' || throttle.requestsPer10Seconds <= 0) {
|
|
150
|
+
throw new Error('requestsPer10Seconds must be a positive number');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build the realm hostname from the realm name
|
|
157
|
+
*/
|
|
158
|
+
export function getRealmHostname(realm: string): string {
|
|
159
|
+
return `${realm}.quickbase.com`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date transformation utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts ISO 8601 date strings in API responses to JavaScript Date objects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ISO 8601 date pattern
|
|
9
|
+
* Matches: 2024-01-15, 2024-01-15T10:30:00, 2024-01-15T10:30:00.000Z, etc.
|
|
10
|
+
*/
|
|
11
|
+
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a string looks like an ISO 8601 date
|
|
15
|
+
*/
|
|
16
|
+
export function isIsoDateString(value: string): boolean {
|
|
17
|
+
return ISO_DATE_PATTERN.test(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Recursively transform ISO date strings to Date objects in an object
|
|
22
|
+
*
|
|
23
|
+
* @param obj - The object to transform
|
|
24
|
+
* @param enabled - Whether to perform conversion (pass false to skip)
|
|
25
|
+
* @returns A new object with date strings converted to Date objects
|
|
26
|
+
*/
|
|
27
|
+
export function transformDates<T>(obj: T, enabled: boolean): T {
|
|
28
|
+
if (!enabled || obj === null || obj === undefined) {
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof obj !== 'object') {
|
|
33
|
+
return obj;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(obj)) {
|
|
37
|
+
return obj.map((item) => transformDates(item, enabled)) as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (obj instanceof Date) {
|
|
41
|
+
return obj;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result: Record<string, unknown> = {};
|
|
45
|
+
|
|
46
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
47
|
+
if (typeof value === 'string' && isIsoDateString(value)) {
|
|
48
|
+
// Convert ISO date string to Date object
|
|
49
|
+
result[key] = new Date(value);
|
|
50
|
+
} else if (value instanceof Date) {
|
|
51
|
+
// Keep existing Date objects as-is
|
|
52
|
+
result[key] = value;
|
|
53
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
54
|
+
// Recursively transform nested objects
|
|
55
|
+
result[key] = transformDates(value, enabled);
|
|
56
|
+
} else {
|
|
57
|
+
// Keep other values as-is
|
|
58
|
+
result[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result as T;
|
|
63
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error types for the QuickBase SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RateLimitInfo } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base error class for all QuickBase SDK errors
|
|
9
|
+
*/
|
|
10
|
+
export class QuickbaseError extends Error {
|
|
11
|
+
readonly statusCode: number;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
readonly rayId?: string;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
message: string,
|
|
17
|
+
statusCode: number,
|
|
18
|
+
options?: {
|
|
19
|
+
description?: string;
|
|
20
|
+
rayId?: string;
|
|
21
|
+
cause?: Error;
|
|
22
|
+
}
|
|
23
|
+
) {
|
|
24
|
+
super(message, { cause: options?.cause });
|
|
25
|
+
this.name = 'QuickbaseError';
|
|
26
|
+
this.statusCode = statusCode;
|
|
27
|
+
this.description = options?.description;
|
|
28
|
+
this.rayId = options?.rayId;
|
|
29
|
+
|
|
30
|
+
// Maintains proper stack trace in V8 environments
|
|
31
|
+
if (Error.captureStackTrace) {
|
|
32
|
+
Error.captureStackTrace(this, QuickbaseError);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toJSON() {
|
|
37
|
+
return {
|
|
38
|
+
name: this.name,
|
|
39
|
+
message: this.message,
|
|
40
|
+
statusCode: this.statusCode,
|
|
41
|
+
description: this.description,
|
|
42
|
+
rayId: this.rayId,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error thrown when rate limited (HTTP 429)
|
|
49
|
+
*/
|
|
50
|
+
export class RateLimitError extends QuickbaseError {
|
|
51
|
+
readonly retryAfter?: number;
|
|
52
|
+
readonly rateLimitInfo: RateLimitInfo;
|
|
53
|
+
|
|
54
|
+
constructor(rateLimitInfo: RateLimitInfo, message?: string) {
|
|
55
|
+
super(
|
|
56
|
+
message || `Rate limited. Retry after ${rateLimitInfo.retryAfter ?? 'unknown'} seconds`,
|
|
57
|
+
429,
|
|
58
|
+
{ rayId: rateLimitInfo.qbApiRay || rateLimitInfo.cfRay }
|
|
59
|
+
);
|
|
60
|
+
this.name = 'RateLimitError';
|
|
61
|
+
this.retryAfter = rateLimitInfo.retryAfter;
|
|
62
|
+
this.rateLimitInfo = rateLimitInfo;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toJSON() {
|
|
66
|
+
return {
|
|
67
|
+
...super.toJSON(),
|
|
68
|
+
retryAfter: this.retryAfter,
|
|
69
|
+
rateLimitInfo: this.rateLimitInfo,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Error thrown when authentication fails (HTTP 401)
|
|
76
|
+
*/
|
|
77
|
+
export class AuthenticationError extends QuickbaseError {
|
|
78
|
+
constructor(message: string, options?: { rayId?: string; cause?: Error }) {
|
|
79
|
+
super(message, 401, options);
|
|
80
|
+
this.name = 'AuthenticationError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Error thrown when authorization fails (HTTP 403)
|
|
86
|
+
*/
|
|
87
|
+
export class AuthorizationError extends QuickbaseError {
|
|
88
|
+
constructor(message: string, options?: { rayId?: string; cause?: Error }) {
|
|
89
|
+
super(message, 403, options);
|
|
90
|
+
this.name = 'AuthorizationError';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Error thrown when a resource is not found (HTTP 404)
|
|
96
|
+
*/
|
|
97
|
+
export class NotFoundError extends QuickbaseError {
|
|
98
|
+
constructor(message: string, options?: { rayId?: string; cause?: Error }) {
|
|
99
|
+
super(message, 404, options);
|
|
100
|
+
this.name = 'NotFoundError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Error thrown for bad requests (HTTP 400)
|
|
106
|
+
*/
|
|
107
|
+
export class ValidationError extends QuickbaseError {
|
|
108
|
+
readonly errors?: Array<{ field?: string; message: string }>;
|
|
109
|
+
|
|
110
|
+
constructor(
|
|
111
|
+
message: string,
|
|
112
|
+
options?: {
|
|
113
|
+
description?: string;
|
|
114
|
+
rayId?: string;
|
|
115
|
+
errors?: Array<{ field?: string; message: string }>;
|
|
116
|
+
cause?: Error;
|
|
117
|
+
}
|
|
118
|
+
) {
|
|
119
|
+
super(message, 400, options);
|
|
120
|
+
this.name = 'ValidationError';
|
|
121
|
+
this.errors = options?.errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
toJSON() {
|
|
125
|
+
return {
|
|
126
|
+
...super.toJSON(),
|
|
127
|
+
errors: this.errors,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Error thrown when request times out
|
|
134
|
+
*/
|
|
135
|
+
export class TimeoutError extends QuickbaseError {
|
|
136
|
+
readonly timeoutMs: number;
|
|
137
|
+
|
|
138
|
+
constructor(timeoutMs: number, options?: { rayId?: string; cause?: Error }) {
|
|
139
|
+
super(`Request timed out after ${timeoutMs}ms`, 0, options);
|
|
140
|
+
this.name = 'TimeoutError';
|
|
141
|
+
this.timeoutMs = timeoutMs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toJSON() {
|
|
145
|
+
return {
|
|
146
|
+
...super.toJSON(),
|
|
147
|
+
timeoutMs: this.timeoutMs,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Error thrown for server errors (HTTP 5xx)
|
|
154
|
+
*/
|
|
155
|
+
export class ServerError extends QuickbaseError {
|
|
156
|
+
constructor(
|
|
157
|
+
statusCode: number,
|
|
158
|
+
message: string,
|
|
159
|
+
options?: { description?: string; rayId?: string; cause?: Error }
|
|
160
|
+
) {
|
|
161
|
+
super(message, statusCode, options);
|
|
162
|
+
this.name = 'ServerError';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Error thrown when an authentication ticket is invalid or expired.
|
|
168
|
+
* Specifically for ticket-based authentication (XML API).
|
|
169
|
+
*/
|
|
170
|
+
export class QuickbaseTicketError extends QuickbaseError {
|
|
171
|
+
readonly code: number;
|
|
172
|
+
|
|
173
|
+
constructor(message: string, code: number, options?: { description?: string; action?: string; cause?: Error }) {
|
|
174
|
+
super(message, 401, options);
|
|
175
|
+
this.name = 'QuickbaseTicketError';
|
|
176
|
+
this.code = code;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
toJSON() {
|
|
180
|
+
return {
|
|
181
|
+
...super.toJSON(),
|
|
182
|
+
code: this.code,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Error thrown when an application token is missing or invalid.
|
|
189
|
+
* Specifically for apps that require app tokens (XML API).
|
|
190
|
+
*/
|
|
191
|
+
export class QuickbaseAppTokenError extends QuickbaseError {
|
|
192
|
+
readonly code: number;
|
|
193
|
+
|
|
194
|
+
constructor(message: string, code: number, options?: { description?: string; action?: string; cause?: Error }) {
|
|
195
|
+
super(message, 403, options);
|
|
196
|
+
this.name = 'QuickbaseAppTokenError';
|
|
197
|
+
this.code = code;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
toJSON() {
|
|
201
|
+
return {
|
|
202
|
+
...super.toJSON(),
|
|
203
|
+
code: this.code,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Error thrown by the legacy QuickBase XML API.
|
|
210
|
+
* Wraps XML-specific error codes and messages.
|
|
211
|
+
*/
|
|
212
|
+
export class QuickbaseXmlError extends QuickbaseError {
|
|
213
|
+
readonly code: number;
|
|
214
|
+
readonly text: string;
|
|
215
|
+
readonly action?: string;
|
|
216
|
+
|
|
217
|
+
constructor(code: number, text: string, options?: { description?: string; action?: string; cause?: Error }) {
|
|
218
|
+
const message = options?.description
|
|
219
|
+
? `XML API error ${code}: ${text} (${options.description})`
|
|
220
|
+
: `XML API error ${code}: ${text}`;
|
|
221
|
+
|
|
222
|
+
// Most XML errors return HTTP 200, so we use 0 or a mapped status if known
|
|
223
|
+
super(message, 0, { rayId: undefined, description: options?.description, cause: options?.cause });
|
|
224
|
+
this.name = 'QuickbaseXmlError';
|
|
225
|
+
this.code = code;
|
|
226
|
+
this.text = text;
|
|
227
|
+
this.action = options?.action;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
toJSON() {
|
|
231
|
+
return {
|
|
232
|
+
...super.toJSON(),
|
|
233
|
+
code: this.code,
|
|
234
|
+
text: this.text,
|
|
235
|
+
action: this.action,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Parse an error response from the QuickBase API
|
|
242
|
+
*/
|
|
243
|
+
export async function parseErrorResponse(
|
|
244
|
+
response: Response,
|
|
245
|
+
requestUrl: string
|
|
246
|
+
): Promise<QuickbaseError> {
|
|
247
|
+
const rayId = response.headers.get('qb-api-ray') || response.headers.get('cf-ray') || undefined;
|
|
248
|
+
|
|
249
|
+
let body: { message?: string; description?: string; errors?: unknown[] } | null = null;
|
|
250
|
+
try {
|
|
251
|
+
body = await response.json() as { message?: string; description?: string; errors?: unknown[] };
|
|
252
|
+
} catch {
|
|
253
|
+
// Response body is not JSON
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const message = body?.message || response.statusText || 'Unknown error';
|
|
257
|
+
const description = body?.description;
|
|
258
|
+
|
|
259
|
+
switch (response.status) {
|
|
260
|
+
case 400:
|
|
261
|
+
return new ValidationError(message, {
|
|
262
|
+
description,
|
|
263
|
+
rayId,
|
|
264
|
+
errors: body?.errors as Array<{ field?: string; message: string }>,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
case 401:
|
|
268
|
+
return new AuthenticationError(message, { rayId });
|
|
269
|
+
|
|
270
|
+
case 403:
|
|
271
|
+
return new AuthorizationError(message, { rayId });
|
|
272
|
+
|
|
273
|
+
case 404:
|
|
274
|
+
return new NotFoundError(message, { rayId });
|
|
275
|
+
|
|
276
|
+
case 429: {
|
|
277
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
278
|
+
const rateLimitInfo: RateLimitInfo = {
|
|
279
|
+
timestamp: new Date(),
|
|
280
|
+
requestUrl,
|
|
281
|
+
httpStatus: 429,
|
|
282
|
+
retryAfter: retryAfter ? parseInt(retryAfter, 10) : undefined,
|
|
283
|
+
cfRay: response.headers.get('cf-ray') || undefined,
|
|
284
|
+
tid: response.headers.get('tid') || undefined,
|
|
285
|
+
qbApiRay: response.headers.get('qb-api-ray') || undefined,
|
|
286
|
+
attempt: 1, // Will be updated by caller
|
|
287
|
+
};
|
|
288
|
+
return new RateLimitError(rateLimitInfo, message);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
default:
|
|
292
|
+
if (response.status >= 500) {
|
|
293
|
+
return new ServerError(response.status, message, { description, rayId });
|
|
294
|
+
}
|
|
295
|
+
return new QuickbaseError(message, response.status, { description, rayId });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if an error is retryable
|
|
301
|
+
*/
|
|
302
|
+
export function isRetryableError(error: unknown): boolean {
|
|
303
|
+
if (error instanceof RateLimitError) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (error instanceof ServerError) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if (error instanceof TimeoutError) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Error thrown when a write operation is attempted in read-only mode.
|
|
317
|
+
* Provides defense-in-depth for both JSON API and XML API requests.
|
|
318
|
+
*/
|
|
319
|
+
export class ReadOnlyError extends Error {
|
|
320
|
+
readonly method: string;
|
|
321
|
+
readonly path: string;
|
|
322
|
+
readonly action?: string; // For XML API actions
|
|
323
|
+
|
|
324
|
+
constructor(method: string, path: string, action?: string) {
|
|
325
|
+
const msg = action
|
|
326
|
+
? `Read-only mode: write operation blocked (XML action: ${action})`
|
|
327
|
+
: `Read-only mode: write operation blocked (${method} ${path})`;
|
|
328
|
+
super(msg);
|
|
329
|
+
this.name = 'ReadOnlyError';
|
|
330
|
+
this.method = method;
|
|
331
|
+
this.path = path;
|
|
332
|
+
this.action = action;
|
|
333
|
+
|
|
334
|
+
// Maintains proper stack trace in V8 environments
|
|
335
|
+
if (Error.captureStackTrace) {
|
|
336
|
+
Error.captureStackTrace(this, ReadOnlyError);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
toJSON() {
|
|
341
|
+
return {
|
|
342
|
+
name: this.name,
|
|
343
|
+
message: this.message,
|
|
344
|
+
method: this.method,
|
|
345
|
+
path: this.path,
|
|
346
|
+
action: this.action,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|