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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
QuickbaseConfig,
|
|
7
|
+
AuthConfig,
|
|
8
|
+
UserTokenAuthConfig,
|
|
9
|
+
TempTokenAuthConfig,
|
|
10
|
+
SsoTokenAuthConfig,
|
|
11
|
+
TicketAuthConfig, // XML-API-TICKET: Remove if XML API discontinued
|
|
12
|
+
RateLimitConfig,
|
|
13
|
+
RetryConfig,
|
|
14
|
+
RequestContext,
|
|
15
|
+
RateLimitInfo,
|
|
16
|
+
PaginationMetadata,
|
|
17
|
+
PaginatedResponse,
|
|
18
|
+
FieldInfo,
|
|
19
|
+
ResolvedConfig,
|
|
20
|
+
// Schema types
|
|
21
|
+
Schema,
|
|
22
|
+
TableSchema,
|
|
23
|
+
FieldSchema,
|
|
24
|
+
ResolvedSchema,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
export { DEFAULT_CONFIG } from './types.js';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
QuickbaseError,
|
|
31
|
+
RateLimitError,
|
|
32
|
+
AuthenticationError,
|
|
33
|
+
AuthorizationError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
TimeoutError,
|
|
37
|
+
ServerError,
|
|
38
|
+
QuickbaseTicketError,
|
|
39
|
+
QuickbaseAppTokenError,
|
|
40
|
+
QuickbaseXmlError,
|
|
41
|
+
ReadOnlyError,
|
|
42
|
+
parseErrorResponse,
|
|
43
|
+
isRetryableError,
|
|
44
|
+
} from './errors.js';
|
|
45
|
+
|
|
46
|
+
export { Logger, createLogger } from './logger.js';
|
|
47
|
+
|
|
48
|
+
export { resolveConfig, getRealmHostname } from './config.js';
|
|
49
|
+
|
|
50
|
+
export { transformDates, isIsoDateString } from './dates.js';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility that respects the debug configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class Logger {
|
|
6
|
+
private readonly enabled: boolean;
|
|
7
|
+
private readonly prefix: string;
|
|
8
|
+
|
|
9
|
+
constructor(enabled: boolean = false, prefix: string = 'quickbase-js') {
|
|
10
|
+
this.enabled = enabled;
|
|
11
|
+
this.prefix = prefix;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private formatMessage(level: string, message: string): string {
|
|
15
|
+
const timestamp = new Date().toISOString();
|
|
16
|
+
return `[${timestamp}] [${this.prefix}] [${level}] ${message}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
debug(message: string, data?: unknown): void {
|
|
20
|
+
if (this.enabled) {
|
|
21
|
+
if (data !== undefined) {
|
|
22
|
+
console.log(this.formatMessage('DEBUG', message), data);
|
|
23
|
+
} else {
|
|
24
|
+
console.log(this.formatMessage('DEBUG', message));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
info(message: string, data?: unknown): void {
|
|
30
|
+
if (this.enabled) {
|
|
31
|
+
if (data !== undefined) {
|
|
32
|
+
console.info(this.formatMessage('INFO', message), data);
|
|
33
|
+
} else {
|
|
34
|
+
console.info(this.formatMessage('INFO', message));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
warn(message: string, data?: unknown): void {
|
|
40
|
+
// Warnings are always logged, regardless of debug setting
|
|
41
|
+
if (data !== undefined) {
|
|
42
|
+
console.warn(this.formatMessage('WARN', message), data);
|
|
43
|
+
} else {
|
|
44
|
+
console.warn(this.formatMessage('WARN', message));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
error(message: string, error?: unknown): void {
|
|
49
|
+
// Errors are always logged, regardless of debug setting
|
|
50
|
+
if (error !== undefined) {
|
|
51
|
+
console.error(this.formatMessage('ERROR', message), error);
|
|
52
|
+
} else {
|
|
53
|
+
console.error(this.formatMessage('ERROR', message));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log rate limit information
|
|
59
|
+
*/
|
|
60
|
+
rateLimit(info: {
|
|
61
|
+
url: string;
|
|
62
|
+
status: number;
|
|
63
|
+
retryAfter?: number;
|
|
64
|
+
attempt: number;
|
|
65
|
+
rayId?: string;
|
|
66
|
+
}): void {
|
|
67
|
+
if (this.enabled) {
|
|
68
|
+
this.debug(
|
|
69
|
+
`Rate limited (attempt ${info.attempt}): ${info.url} - ` +
|
|
70
|
+
`Status ${info.status}, Retry-After: ${info.retryAfter ?? 'not specified'}s` +
|
|
71
|
+
(info.rayId ? `, Ray: ${info.rayId}` : '')
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Log request timing
|
|
78
|
+
*/
|
|
79
|
+
timing(method: string, url: string, durationMs: number): void {
|
|
80
|
+
if (this.enabled) {
|
|
81
|
+
this.debug(`${method} ${url} completed in ${durationMs}ms`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Log retry attempt
|
|
87
|
+
*/
|
|
88
|
+
retry(attempt: number, maxAttempts: number, delayMs: number, reason: string): void {
|
|
89
|
+
if (this.enabled) {
|
|
90
|
+
this.debug(
|
|
91
|
+
`Retry ${attempt}/${maxAttempts} in ${delayMs}ms: ${reason}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Log token operations (without exposing the actual token)
|
|
98
|
+
*/
|
|
99
|
+
token(operation: 'fetch' | 'cache-hit' | 'cache-miss' | 'refresh' | 'expire', dbid?: string): void {
|
|
100
|
+
if (this.enabled) {
|
|
101
|
+
const target = dbid ? ` for dbid: ${dbid}` : '';
|
|
102
|
+
this.debug(`Token ${operation}${target}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a no-op logger for when logging is disabled
|
|
109
|
+
* This avoids conditional checks at every log call
|
|
110
|
+
*/
|
|
111
|
+
export function createLogger(enabled: boolean): Logger {
|
|
112
|
+
return new Logger(enabled);
|
|
113
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema resolution and lookup utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Schema, ResolvedSchema } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Error thrown when an unknown table or field alias is used
|
|
9
|
+
*/
|
|
10
|
+
export class SchemaError extends Error {
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'SchemaError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a schema definition into lookup maps for efficient alias resolution
|
|
19
|
+
*/
|
|
20
|
+
export function resolveSchema(schema?: Schema): ResolvedSchema | undefined {
|
|
21
|
+
if (!schema) return undefined;
|
|
22
|
+
|
|
23
|
+
const tableAliasToId = new Map<string, string>();
|
|
24
|
+
const tableIdToAlias = new Map<string, string>();
|
|
25
|
+
const fieldAliasToId = new Map<string, Map<string, number>>();
|
|
26
|
+
const fieldIdToAlias = new Map<string, Map<number, string>>();
|
|
27
|
+
|
|
28
|
+
for (const [tableAlias, tableSchema] of Object.entries(schema.tables)) {
|
|
29
|
+
const tableId = tableSchema.id;
|
|
30
|
+
|
|
31
|
+
// Table mappings
|
|
32
|
+
tableAliasToId.set(tableAlias, tableId);
|
|
33
|
+
tableIdToAlias.set(tableId, tableAlias);
|
|
34
|
+
|
|
35
|
+
// Field mappings for this table
|
|
36
|
+
const aliasToId = new Map<string, number>();
|
|
37
|
+
const idToAlias = new Map<number, string>();
|
|
38
|
+
|
|
39
|
+
for (const [fieldAlias, fieldId] of Object.entries(tableSchema.fields)) {
|
|
40
|
+
aliasToId.set(fieldAlias, fieldId);
|
|
41
|
+
idToAlias.set(fieldId, fieldAlias);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fieldAliasToId.set(tableId, aliasToId);
|
|
45
|
+
fieldIdToAlias.set(tableId, idToAlias);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
original: schema,
|
|
50
|
+
tableAliasToId,
|
|
51
|
+
tableIdToAlias,
|
|
52
|
+
fieldAliasToId,
|
|
53
|
+
fieldIdToAlias,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a table alias to its ID.
|
|
59
|
+
* If the input is already a table ID, returns it unchanged.
|
|
60
|
+
* Throws SchemaError if alias is not found.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveTableAlias(
|
|
63
|
+
schema: ResolvedSchema | undefined,
|
|
64
|
+
tableRef: string
|
|
65
|
+
): string {
|
|
66
|
+
if (!schema) return tableRef;
|
|
67
|
+
|
|
68
|
+
// Check if it's an alias
|
|
69
|
+
const tableId = schema.tableAliasToId.get(tableRef);
|
|
70
|
+
if (tableId) return tableId;
|
|
71
|
+
|
|
72
|
+
// Check if it's already a table ID
|
|
73
|
+
if (schema.tableIdToAlias.has(tableRef)) return tableRef;
|
|
74
|
+
|
|
75
|
+
// Unknown alias - throw helpful error
|
|
76
|
+
const availableAliases = Array.from(schema.tableAliasToId.keys());
|
|
77
|
+
const suggestion = findSimilar(tableRef, availableAliases);
|
|
78
|
+
const suggestionText = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
79
|
+
const availableText =
|
|
80
|
+
availableAliases.length > 0
|
|
81
|
+
? ` Available: ${availableAliases.join(', ')}`
|
|
82
|
+
: '';
|
|
83
|
+
|
|
84
|
+
throw new SchemaError(
|
|
85
|
+
`Unknown table alias '${tableRef}'.${suggestionText}${availableText}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a field alias to its ID for a given table.
|
|
91
|
+
* If the input is already a field ID (number), returns it unchanged.
|
|
92
|
+
* Throws SchemaError if alias is not found.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveFieldAlias(
|
|
95
|
+
schema: ResolvedSchema | undefined,
|
|
96
|
+
tableId: string,
|
|
97
|
+
fieldRef: string | number
|
|
98
|
+
): number {
|
|
99
|
+
// If it's already a number, return as-is
|
|
100
|
+
if (typeof fieldRef === 'number') return fieldRef;
|
|
101
|
+
|
|
102
|
+
// If no schema, try to parse as number or return as-is
|
|
103
|
+
if (!schema) {
|
|
104
|
+
const num = parseInt(fieldRef, 10);
|
|
105
|
+
if (!isNaN(num)) return num;
|
|
106
|
+
// Can't resolve without schema - pass through and let API handle it
|
|
107
|
+
return fieldRef as unknown as number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Look up the alias
|
|
111
|
+
const fieldMap = schema.fieldAliasToId.get(tableId);
|
|
112
|
+
if (fieldMap) {
|
|
113
|
+
const fieldId = fieldMap.get(fieldRef);
|
|
114
|
+
if (fieldId !== undefined) return fieldId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if it's a numeric string (raw field ID)
|
|
118
|
+
const num = parseInt(fieldRef, 10);
|
|
119
|
+
if (!isNaN(num)) return num;
|
|
120
|
+
|
|
121
|
+
// Unknown alias - throw helpful error
|
|
122
|
+
const availableAliases = fieldMap ? Array.from(fieldMap.keys()) : [];
|
|
123
|
+
const suggestion = findSimilar(fieldRef, availableAliases);
|
|
124
|
+
const suggestionText = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
125
|
+
|
|
126
|
+
// Get table alias for better error message
|
|
127
|
+
const tableAlias = schema.tableIdToAlias.get(tableId) || tableId;
|
|
128
|
+
|
|
129
|
+
throw new SchemaError(
|
|
130
|
+
`Unknown field alias '${fieldRef}' in table '${tableAlias}'.${suggestionText}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the alias for a field ID, if defined in schema.
|
|
136
|
+
* Returns undefined if no alias is defined.
|
|
137
|
+
*/
|
|
138
|
+
export function getFieldAlias(
|
|
139
|
+
schema: ResolvedSchema | undefined,
|
|
140
|
+
tableId: string,
|
|
141
|
+
fieldId: number
|
|
142
|
+
): string | undefined {
|
|
143
|
+
if (!schema) return undefined;
|
|
144
|
+
|
|
145
|
+
const fieldMap = schema.fieldIdToAlias.get(tableId);
|
|
146
|
+
return fieldMap?.get(fieldId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get the alias for a table ID, if defined in schema.
|
|
151
|
+
* Returns undefined if no alias is defined.
|
|
152
|
+
*/
|
|
153
|
+
export function getTableAlias(
|
|
154
|
+
schema: ResolvedSchema | undefined,
|
|
155
|
+
tableId: string
|
|
156
|
+
): string | undefined {
|
|
157
|
+
if (!schema) return undefined;
|
|
158
|
+
return schema.tableIdToAlias.get(tableId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Find a similar string from a list (for "did you mean" suggestions)
|
|
163
|
+
* Uses simple Levenshtein distance
|
|
164
|
+
*/
|
|
165
|
+
function findSimilar(
|
|
166
|
+
input: string,
|
|
167
|
+
candidates: string[],
|
|
168
|
+
maxDistance = 3
|
|
169
|
+
): string | undefined {
|
|
170
|
+
const inputLower = input.toLowerCase();
|
|
171
|
+
let bestMatch: string | undefined;
|
|
172
|
+
let bestDistance = maxDistance + 1;
|
|
173
|
+
|
|
174
|
+
for (const candidate of candidates) {
|
|
175
|
+
const distance = levenshteinDistance(inputLower, candidate.toLowerCase());
|
|
176
|
+
if (distance < bestDistance) {
|
|
177
|
+
bestDistance = distance;
|
|
178
|
+
bestMatch = candidate;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return bestDistance <= maxDistance ? bestMatch : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Calculate Levenshtein distance between two strings
|
|
187
|
+
*/
|
|
188
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
189
|
+
if (a.length === 0) return b.length;
|
|
190
|
+
if (b.length === 0) return a.length;
|
|
191
|
+
|
|
192
|
+
// Create matrix with proper initialization
|
|
193
|
+
const matrix: number[][] = Array.from({ length: b.length + 1 }, (_, i) =>
|
|
194
|
+
Array.from({ length: a.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Fill in the rest
|
|
198
|
+
for (let i = 1; i <= b.length; i++) {
|
|
199
|
+
for (let j = 1; j <= a.length; j++) {
|
|
200
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
201
|
+
matrix[i]![j] = matrix[i - 1]![j - 1]!;
|
|
202
|
+
} else {
|
|
203
|
+
matrix[i]![j] = Math.min(
|
|
204
|
+
matrix[i - 1]![j - 1]! + 1, // substitution
|
|
205
|
+
matrix[i]![j - 1]! + 1, // insertion
|
|
206
|
+
matrix[i - 1]![j]! + 1 // deletion
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return matrix[b.length]![a.length]!;
|
|
213
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request and response transformation utilities for schema aliases
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ResolvedSchema } from './types.js';
|
|
6
|
+
import {
|
|
7
|
+
resolveTableAlias,
|
|
8
|
+
resolveFieldAlias,
|
|
9
|
+
getFieldAlias,
|
|
10
|
+
} from './schema.js';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Request Transformation
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context for request transformation
|
|
18
|
+
*/
|
|
19
|
+
export interface RequestTransformContext {
|
|
20
|
+
schema?: ResolvedSchema;
|
|
21
|
+
/** The resolved table ID (after alias resolution) */
|
|
22
|
+
tableId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Transform a request body, resolving table and field aliases to IDs.
|
|
27
|
+
* Handles: from, to, select, sortBy, groupBy, where, data
|
|
28
|
+
*/
|
|
29
|
+
export function transformRequest<T extends Record<string, unknown>>(
|
|
30
|
+
body: T,
|
|
31
|
+
ctx: RequestTransformContext
|
|
32
|
+
): T {
|
|
33
|
+
// Work with a plain object to avoid TypeScript index signature issues
|
|
34
|
+
const result: Record<string, unknown> = { ...body };
|
|
35
|
+
let tableId = ctx.tableId;
|
|
36
|
+
|
|
37
|
+
// Schema-dependent transforms (alias resolution)
|
|
38
|
+
if (ctx.schema) {
|
|
39
|
+
// Resolve table references (from, to)
|
|
40
|
+
if ('from' in result && typeof result.from === 'string') {
|
|
41
|
+
tableId = resolveTableAlias(ctx.schema, result.from);
|
|
42
|
+
result.from = tableId;
|
|
43
|
+
}
|
|
44
|
+
if ('to' in result && typeof result.to === 'string') {
|
|
45
|
+
tableId = resolveTableAlias(ctx.schema, result.to);
|
|
46
|
+
result.to = tableId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve field aliases (requires tableId)
|
|
50
|
+
if (tableId) {
|
|
51
|
+
// Resolve select array
|
|
52
|
+
if ('select' in result && Array.isArray(result.select)) {
|
|
53
|
+
result.select = (result.select as (string | number)[]).map((field) =>
|
|
54
|
+
resolveFieldAlias(ctx.schema!, tableId!, field)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Resolve sortBy array
|
|
59
|
+
if ('sortBy' in result && Array.isArray(result.sortBy)) {
|
|
60
|
+
result.sortBy = (result.sortBy as Record<string, unknown>[]).map((sort) => ({
|
|
61
|
+
...sort,
|
|
62
|
+
fieldId: resolveFieldAlias(ctx.schema!, tableId!, sort.fieldId as string | number),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolve groupBy array
|
|
67
|
+
if ('groupBy' in result && Array.isArray(result.groupBy)) {
|
|
68
|
+
result.groupBy = (result.groupBy as Record<string, unknown>[]).map((group) => ({
|
|
69
|
+
...group,
|
|
70
|
+
fieldId: resolveFieldAlias(ctx.schema!, tableId!, group.fieldId as string | number),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Resolve where clause (string replacement)
|
|
75
|
+
if ('where' in result && typeof result.where === 'string') {
|
|
76
|
+
result.where = transformWhereClause(result.where, ctx.schema, tableId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-wrap data values (always runs, no schema required)
|
|
82
|
+
if ('data' in result && Array.isArray(result.data)) {
|
|
83
|
+
result.data = (result.data as Record<string, unknown>[]).map((record) =>
|
|
84
|
+
wrapRecordValues(record, ctx.schema, tableId)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result as T;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wrap record values and optionally resolve field aliases.
|
|
93
|
+
* Always wraps plain values in { value: X } format.
|
|
94
|
+
* Resolves field aliases to IDs if schema is provided.
|
|
95
|
+
*/
|
|
96
|
+
function wrapRecordValues(
|
|
97
|
+
record: Record<string, unknown>,
|
|
98
|
+
schema: ResolvedSchema | undefined,
|
|
99
|
+
tableId: string | undefined
|
|
100
|
+
): Record<string, unknown> {
|
|
101
|
+
const result: Record<string, unknown> = {};
|
|
102
|
+
|
|
103
|
+
for (const [key, value] of Object.entries(record)) {
|
|
104
|
+
// Resolve field alias if schema is available
|
|
105
|
+
let outputKey = key;
|
|
106
|
+
if (schema && tableId) {
|
|
107
|
+
try {
|
|
108
|
+
const fieldId = resolveFieldAlias(schema, tableId, key);
|
|
109
|
+
outputKey = String(fieldId);
|
|
110
|
+
} catch {
|
|
111
|
+
// Not an alias, keep original key
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Always auto-wrap values
|
|
116
|
+
result[outputKey] = wrapFieldValue(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Transform a where clause, replacing field aliases with IDs.
|
|
124
|
+
* QuickBase where syntax: {fieldId.operator.value}
|
|
125
|
+
* Example: {'status'.EX.'Active'} -> {7.EX.'Active'}
|
|
126
|
+
*/
|
|
127
|
+
function transformWhereClause(
|
|
128
|
+
where: string,
|
|
129
|
+
schema: ResolvedSchema,
|
|
130
|
+
tableId: string
|
|
131
|
+
): string {
|
|
132
|
+
// Match patterns like {'fieldAlias' or {fieldAlias. at the start of conditions
|
|
133
|
+
// The pattern captures the field reference between { and .
|
|
134
|
+
return where.replace(/\{['"]?([^.'"\}]+)['"]?\./g, (match, fieldRef: string) => {
|
|
135
|
+
try {
|
|
136
|
+
const fieldId = resolveFieldAlias(schema, tableId, fieldRef);
|
|
137
|
+
return `{${fieldId}.`;
|
|
138
|
+
} catch {
|
|
139
|
+
// If alias not found, keep original (might be a raw ID)
|
|
140
|
+
return match;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Wrap a value in QuickBase's { value: X } format if not already wrapped.
|
|
147
|
+
* Handles plain values, arrays, and already-wrapped values.
|
|
148
|
+
*/
|
|
149
|
+
function wrapFieldValue(value: unknown): { value: unknown } {
|
|
150
|
+
// Already wrapped - return as-is
|
|
151
|
+
if (isWrappedValue(value)) {
|
|
152
|
+
return value as { value: unknown };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Wrap plain value
|
|
156
|
+
return { value };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a value is already wrapped in { value: X } format.
|
|
161
|
+
*/
|
|
162
|
+
function isWrappedValue(value: unknown): boolean {
|
|
163
|
+
if (value === null || value === undefined) return false;
|
|
164
|
+
if (typeof value !== 'object' || Array.isArray(value)) return false;
|
|
165
|
+
|
|
166
|
+
const obj = value as Record<string, unknown>;
|
|
167
|
+
return 'value' in obj;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Response Transformation
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Context for response transformation
|
|
176
|
+
*/
|
|
177
|
+
export interface ResponseTransformContext {
|
|
178
|
+
schema?: ResolvedSchema;
|
|
179
|
+
/** The table ID for field alias lookups */
|
|
180
|
+
tableId?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Transform a response, converting field IDs to aliases and unwrapping values.
|
|
185
|
+
* Handles nested data arrays (for runQuery, upsert responses).
|
|
186
|
+
*/
|
|
187
|
+
export function transformResponse<T>(
|
|
188
|
+
response: T,
|
|
189
|
+
ctx: ResponseTransformContext
|
|
190
|
+
): T {
|
|
191
|
+
if (!response || typeof response !== 'object') return response;
|
|
192
|
+
|
|
193
|
+
const result = { ...response } as Record<string, unknown>;
|
|
194
|
+
|
|
195
|
+
// Handle data array (runQuery, upsert responses)
|
|
196
|
+
if ('data' in result && Array.isArray(result.data) && ctx.tableId) {
|
|
197
|
+
result.data = result.data.map((record) =>
|
|
198
|
+
transformRecordForResponse(
|
|
199
|
+
record as Record<string, unknown>,
|
|
200
|
+
ctx.schema,
|
|
201
|
+
ctx.tableId!
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result as T;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Transform a single record from the response.
|
|
211
|
+
* - Converts field ID keys to aliases (if schema defined)
|
|
212
|
+
* - Unwraps { value: X } to just X
|
|
213
|
+
* - Unknown fields keep their numeric key but still get unwrapped
|
|
214
|
+
*/
|
|
215
|
+
function transformRecordForResponse(
|
|
216
|
+
record: Record<string, unknown>,
|
|
217
|
+
schema: ResolvedSchema | undefined,
|
|
218
|
+
tableId: string
|
|
219
|
+
): Record<string, unknown> {
|
|
220
|
+
const result: Record<string, unknown> = {};
|
|
221
|
+
|
|
222
|
+
for (const [key, value] of Object.entries(record)) {
|
|
223
|
+
// Determine the output key (alias or original)
|
|
224
|
+
let outputKey = key;
|
|
225
|
+
const fieldId = parseInt(key, 10);
|
|
226
|
+
|
|
227
|
+
if (!isNaN(fieldId) && schema) {
|
|
228
|
+
const alias = getFieldAlias(schema, tableId, fieldId);
|
|
229
|
+
if (alias) {
|
|
230
|
+
outputKey = alias;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Unwrap { value: X } -> X
|
|
235
|
+
const unwrappedValue = unwrapFieldValue(value);
|
|
236
|
+
result[outputKey] = unwrappedValue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Unwrap a field value from QuickBase's { value: X } format.
|
|
244
|
+
* Also handles arrays of values.
|
|
245
|
+
*/
|
|
246
|
+
function unwrapFieldValue(value: unknown): unknown {
|
|
247
|
+
if (value === null || value === undefined) return value;
|
|
248
|
+
|
|
249
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
250
|
+
const obj = value as Record<string, unknown>;
|
|
251
|
+
// Check if it's a { value: X } wrapper
|
|
252
|
+
if ('value' in obj && Object.keys(obj).length === 1) {
|
|
253
|
+
// Recursively unwrap in case value is an array of wrapped values
|
|
254
|
+
return unwrapFieldValue(obj.value);
|
|
255
|
+
}
|
|
256
|
+
// Check if it's a { value: X, ...metadata } wrapper (preserve just value)
|
|
257
|
+
if ('value' in obj) {
|
|
258
|
+
return unwrapFieldValue(obj.value);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Handle array of { value: X } objects
|
|
263
|
+
if (Array.isArray(value)) {
|
|
264
|
+
return value.map(unwrapFieldValue);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// =============================================================================
|
|
271
|
+
// Table ID Extraction
|
|
272
|
+
// =============================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extract the table ID from a request body.
|
|
276
|
+
* Looks for 'from' or 'to' fields and resolves any aliases.
|
|
277
|
+
*/
|
|
278
|
+
export function extractTableIdFromRequest(
|
|
279
|
+
body: Record<string, unknown> | undefined,
|
|
280
|
+
schema?: ResolvedSchema
|
|
281
|
+
): string | undefined {
|
|
282
|
+
if (!body) return undefined;
|
|
283
|
+
|
|
284
|
+
// Check 'from' (runQuery, deleteRecords)
|
|
285
|
+
if ('from' in body && typeof body.from === 'string') {
|
|
286
|
+
return schema ? resolveTableAlias(schema, body.from) : body.from;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check 'to' (upsert)
|
|
290
|
+
if ('to' in body && typeof body.to === 'string') {
|
|
291
|
+
return schema ? resolveTableAlias(schema, body.to) : body.to;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|