quickbase-js 2.4.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.
Files changed (46) hide show
  1. package/README.md +144 -76
  2. package/dist/quickbase.esm.js +3 -3
  3. package/dist/quickbase.min.js +3 -3
  4. package/dist/quickbase.min.js.map +1 -1
  5. package/package.json +13 -9
  6. package/src/auth/index.ts +55 -0
  7. package/src/auth/sso-token.ts +143 -0
  8. package/src/auth/temp-token.ts +160 -0
  9. package/src/auth/ticket.ts +329 -0
  10. package/src/auth/token-cache.ts +97 -0
  11. package/src/auth/types.ts +50 -0
  12. package/src/auth/user-token.ts +38 -0
  13. package/src/cli/schema.ts +512 -0
  14. package/src/client/index.ts +36 -0
  15. package/src/client/pagination.ts +337 -0
  16. package/src/client/request.ts +544 -0
  17. package/src/client/retry.ts +169 -0
  18. package/src/client/throttle.ts +105 -0
  19. package/src/core/config.ts +160 -0
  20. package/src/core/dates.ts +63 -0
  21. package/src/core/errors.ts +349 -0
  22. package/src/core/index.ts +50 -0
  23. package/src/core/logger.ts +113 -0
  24. package/src/core/schema.ts +213 -0
  25. package/src/core/transform.ts +295 -0
  26. package/src/core/types.ts +337 -0
  27. package/src/generated/client.ts +1785 -0
  28. package/src/generated/operations.json +672 -0
  29. package/src/generated/types.ts +4884 -0
  30. package/src/index.ts +259 -0
  31. package/src/xml/codepages.ts +119 -0
  32. package/src/xml/dbvars.ts +75 -0
  33. package/src/xml/errors.ts +133 -0
  34. package/src/xml/fields.ts +121 -0
  35. package/src/xml/groups.ts +560 -0
  36. package/src/xml/html.ts +152 -0
  37. package/src/xml/index.ts +1196 -0
  38. package/src/xml/metadata.ts +111 -0
  39. package/src/xml/records.ts +301 -0
  40. package/src/xml/request.ts +238 -0
  41. package/src/xml/roles.ts +321 -0
  42. package/src/xml/schema.ts +436 -0
  43. package/src/xml/transform.ts +381 -0
  44. package/src/xml/types.ts +602 -0
  45. package/src/xml/users.ts +221 -0
  46. package/src/xml/webhooks.ts +260 -0
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI Schema Generator
4
+ *
5
+ * Generates a schema definition from a QuickBase application.
6
+ *
7
+ * Usage:
8
+ * npx quickbase-js schema --realm <realm> --app <appId> --token <token>
9
+ *
10
+ * Options:
11
+ * --realm QuickBase realm (e.g., "mycompany")
12
+ * --app Application ID (e.g., "bqw123abc")
13
+ * --token User token for authentication (or set QB_USER_TOKEN env var)
14
+ * --output Output file path (default: stdout)
15
+ * --format Output format: "ts" or "json" (default: "ts")
16
+ * --merge Merge with existing schema file, preserving custom aliases
17
+ * --help Show help
18
+ */
19
+
20
+ import { createClient } from '../index.js';
21
+ import type { Schema, TableSchema } from '../core/types.js';
22
+
23
+ interface CliOptions {
24
+ realm?: string;
25
+ app?: string;
26
+ token?: string;
27
+ output?: string;
28
+ format?: 'ts' | 'json';
29
+ merge?: boolean;
30
+ help?: boolean;
31
+ }
32
+
33
+ /**
34
+ * Parse command line arguments
35
+ */
36
+ function parseArgs(args: string[]): CliOptions {
37
+ const options: CliOptions = {};
38
+
39
+ for (let i = 0; i < args.length; i++) {
40
+ const arg = args[i];
41
+ const next = args[i + 1];
42
+
43
+ switch (arg) {
44
+ case '--realm':
45
+ case '-r':
46
+ options.realm = next;
47
+ i++;
48
+ break;
49
+ case '--app':
50
+ case '-a':
51
+ options.app = next;
52
+ i++;
53
+ break;
54
+ case '--token':
55
+ case '-t':
56
+ options.token = next;
57
+ i++;
58
+ break;
59
+ case '--output':
60
+ case '-o':
61
+ options.output = next;
62
+ i++;
63
+ break;
64
+ case '--format':
65
+ case '-f':
66
+ options.format = next as 'ts' | 'json';
67
+ i++;
68
+ break;
69
+ case '--help':
70
+ case '-h':
71
+ options.help = true;
72
+ break;
73
+ case '--merge':
74
+ case '-m':
75
+ options.merge = true;
76
+ break;
77
+ }
78
+ }
79
+
80
+ return options;
81
+ }
82
+
83
+ /**
84
+ * Show help message
85
+ */
86
+ function showHelp(): void {
87
+ console.log(`
88
+ quickbase-js schema - Generate schema from QuickBase application
89
+
90
+ Usage:
91
+ npx quickbase-js schema [options]
92
+
93
+ Options:
94
+ -r, --realm <realm> QuickBase realm (required, e.g., "mycompany")
95
+ -a, --app <appId> Application ID (required, e.g., "bqw123abc")
96
+ -t, --token <token> User token (or set QB_USER_TOKEN env var)
97
+ -o, --output <file> Output file path (default: stdout)
98
+ -f, --format <type> Output format: "ts" or "json" (default: "ts")
99
+ -m, --merge Merge with existing schema, preserving custom aliases
100
+ -h, --help Show this help message
101
+
102
+ Examples:
103
+ # Generate TypeScript schema to stdout
104
+ npx quickbase-js schema -r mycompany -a bqw123abc -t your-token
105
+
106
+ # Generate and save to file
107
+ npx quickbase-js schema -r mycompany -a bqw123abc --output schema.ts
108
+
109
+ # Generate JSON format
110
+ npx quickbase-js schema -r mycompany -a bqw123abc -f json -o schema.json
111
+
112
+ # Update existing schema with new fields (preserves custom aliases)
113
+ npx quickbase-js schema -r mycompany -a bqw123abc -o schema.ts --merge
114
+
115
+ # Using environment variable for token
116
+ QB_USER_TOKEN=your-token npx quickbase-js schema -r mycompany -a bqw123abc
117
+ `);
118
+ }
119
+
120
+ /**
121
+ * Convert a label to a camelCase alias
122
+ */
123
+ function labelToAlias(label: string): string {
124
+ // Remove non-alphanumeric chars except spaces
125
+ const cleaned = label
126
+ .replace(/[^a-zA-Z0-9\s]/g, '')
127
+ .trim();
128
+
129
+ // Handle empty string
130
+ if (!cleaned) return 'field';
131
+
132
+ // Split by spaces and convert to camelCase
133
+ const words = cleaned.split(/\s+/);
134
+ return words
135
+ .map((word, i) => {
136
+ const lower = word.toLowerCase();
137
+ if (i === 0) return lower;
138
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
139
+ })
140
+ .join('');
141
+ }
142
+
143
+ /**
144
+ * Make alias unique by appending number if needed
145
+ */
146
+ function makeUnique(alias: string, existing: Set<string>): string {
147
+ if (!existing.has(alias)) {
148
+ existing.add(alias);
149
+ return alias;
150
+ }
151
+
152
+ let counter = 2;
153
+ while (existing.has(`${alias}${counter}`)) {
154
+ counter++;
155
+ }
156
+ const unique = `${alias}${counter}`;
157
+ existing.add(unique);
158
+ return unique;
159
+ }
160
+
161
+ /**
162
+ * Fetch schema from QuickBase application
163
+ */
164
+ async function fetchSchema(
165
+ realm: string,
166
+ appId: string,
167
+ token: string
168
+ ): Promise<Schema> {
169
+ const client = createClient({
170
+ realm,
171
+ auth: { type: 'user-token', userToken: token },
172
+ });
173
+
174
+ // Fetch all tables
175
+ const tables = await client.getAppTables({ appId });
176
+
177
+ const schema: Schema = { tables: {} };
178
+
179
+ for (const table of tables) {
180
+ if (!table.id || !table.name) continue;
181
+
182
+ // Generate table alias from name
183
+ const tableAlias = labelToAlias(table.name);
184
+
185
+ // Fetch fields for this table
186
+ const fields = await client.getFields({ tableId: table.id });
187
+
188
+ const fieldAliases = new Set<string>();
189
+ const fieldMap: Record<string, number> = {};
190
+
191
+ for (const field of fields) {
192
+ if (!field.label) continue;
193
+
194
+ // Generate field alias from label
195
+ let alias = labelToAlias(field.label);
196
+ alias = makeUnique(alias, fieldAliases);
197
+
198
+ fieldMap[alias] = field.id;
199
+ }
200
+
201
+ schema.tables[tableAlias] = {
202
+ id: table.id,
203
+ fields: fieldMap,
204
+ };
205
+ }
206
+
207
+ return schema;
208
+ }
209
+
210
+ /**
211
+ * Format schema as TypeScript
212
+ */
213
+ function formatAsTypeScript(schema: Schema): string {
214
+ const lines: string[] = [
215
+ '/**',
216
+ ' * QuickBase Schema Definition',
217
+ ' * Generated by: npx quickbase-js schema',
218
+ ` * Generated at: ${new Date().toISOString()}`,
219
+ ' */',
220
+ '',
221
+ "import type { Schema } from 'quickbase-js';",
222
+ '',
223
+ 'export const schema: Schema = {',
224
+ ' tables: {',
225
+ ];
226
+
227
+ const tableAliases = Object.keys(schema.tables);
228
+ for (let t = 0; t < tableAliases.length; t++) {
229
+ const tableAlias = tableAliases[t]!;
230
+ const table = schema.tables[tableAlias]!;
231
+ const isLastTable = t === tableAliases.length - 1;
232
+
233
+ lines.push(` ${tableAlias}: {`);
234
+ lines.push(` id: '${table.id}',`);
235
+ lines.push(' fields: {');
236
+
237
+ const fieldAliases = Object.keys(table.fields);
238
+ for (let f = 0; f < fieldAliases.length; f++) {
239
+ const fieldAlias = fieldAliases[f]!;
240
+ const fieldId = table.fields[fieldAlias]!;
241
+ const isLastField = f === fieldAliases.length - 1;
242
+ const comma = isLastField ? '' : ',';
243
+ lines.push(` ${fieldAlias}: ${fieldId}${comma}`);
244
+ }
245
+
246
+ lines.push(' },');
247
+ lines.push(` }${isLastTable ? '' : ','}`);
248
+ }
249
+
250
+ lines.push(' },');
251
+ lines.push('};');
252
+ lines.push('');
253
+
254
+ return lines.join('\n');
255
+ }
256
+
257
+ /**
258
+ * Format schema as JSON
259
+ */
260
+ function formatAsJson(schema: Schema): string {
261
+ return JSON.stringify(schema, null, 2) + '\n';
262
+ }
263
+
264
+ /**
265
+ * Load existing schema from a file
266
+ */
267
+ async function loadExistingSchema(filePath: string, format: 'ts' | 'json'): Promise<Schema | null> {
268
+ const fs = await import('fs');
269
+
270
+ if (!fs.existsSync(filePath)) {
271
+ return null;
272
+ }
273
+
274
+ const content = fs.readFileSync(filePath, 'utf-8');
275
+
276
+ if (format === 'json') {
277
+ try {
278
+ return JSON.parse(content) as Schema;
279
+ } catch {
280
+ console.error(`Warning: Could not parse existing JSON schema at ${filePath}`);
281
+ return null;
282
+ }
283
+ }
284
+
285
+ // Parse TypeScript format - extract the schema object
286
+ // Look for pattern: export const schema: Schema = { ... }
287
+ const schemaMatch = content.match(/export\s+const\s+schema[^=]*=\s*(\{[\s\S]*\});?\s*$/m);
288
+ if (!schemaMatch) {
289
+ console.error(`Warning: Could not parse existing TypeScript schema at ${filePath}`);
290
+ return null;
291
+ }
292
+
293
+ try {
294
+ // Use Function constructor to safely evaluate the object literal
295
+ // This handles the JS object syntax in the TS file
296
+ const schemaStr = schemaMatch[1]!;
297
+ const fn = new Function(`return ${schemaStr}`);
298
+ return fn() as Schema;
299
+ } catch (e) {
300
+ console.error(`Warning: Could not evaluate existing schema: ${e}`);
301
+ return null;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Build reverse lookup: field ID -> alias for a table
307
+ */
308
+ function buildFieldIdToAlias(table: TableSchema): Map<number, string> {
309
+ const map = new Map<number, string>();
310
+ for (const [alias, id] of Object.entries(table.fields)) {
311
+ map.set(id, alias);
312
+ }
313
+ return map;
314
+ }
315
+
316
+ /**
317
+ * Build reverse lookup: table ID -> alias
318
+ */
319
+ function buildTableIdToAlias(schema: Schema): Map<string, string> {
320
+ const map = new Map<string, string>();
321
+ for (const [alias, table] of Object.entries(schema.tables)) {
322
+ map.set(table.id, alias);
323
+ }
324
+ return map;
325
+ }
326
+
327
+ /**
328
+ * Merge new schema with existing schema, preserving custom aliases
329
+ */
330
+ function mergeSchemas(existing: Schema, fresh: Schema): { merged: Schema; stats: MergeStats } {
331
+ const stats: MergeStats = {
332
+ tablesAdded: 0,
333
+ tablesRemoved: 0,
334
+ tablesPreserved: 0,
335
+ fieldsAdded: 0,
336
+ fieldsRemoved: 0,
337
+ fieldsPreserved: 0,
338
+ };
339
+
340
+ const merged: Schema = { tables: {} };
341
+
342
+ // Build reverse lookup for existing schema (ID -> alias)
343
+ const existingTableIdToAlias = buildTableIdToAlias(existing);
344
+ const existingFieldIdToAlias = new Map<string, Map<number, string>>();
345
+ for (const [, table] of Object.entries(existing.tables)) {
346
+ existingFieldIdToAlias.set(table.id, buildFieldIdToAlias(table));
347
+ }
348
+
349
+ // Track which existing tables we've seen (by ID)
350
+ const seenTableIds = new Set<string>();
351
+
352
+ // Process each table from fresh schema
353
+ for (const [freshTableAlias, freshTable] of Object.entries(fresh.tables)) {
354
+ const tableId = freshTable.id;
355
+ seenTableIds.add(tableId);
356
+
357
+ // Use existing alias if available, otherwise use fresh alias
358
+ const tableAlias = existingTableIdToAlias.get(tableId) ?? freshTableAlias;
359
+ const existingFieldMap = existingFieldIdToAlias.get(tableId);
360
+
361
+ // Track which existing fields we've seen (by ID)
362
+ const seenFieldIds = new Set<number>();
363
+ const mergedFields: Record<string, number> = {};
364
+
365
+ // Process each field from fresh schema
366
+ for (const [freshFieldAlias, fieldId] of Object.entries(freshTable.fields)) {
367
+ seenFieldIds.add(fieldId);
368
+
369
+ // Use existing alias if available, otherwise use fresh alias
370
+ const fieldAlias = existingFieldMap?.get(fieldId) ?? freshFieldAlias;
371
+ mergedFields[fieldAlias] = fieldId;
372
+
373
+ if (existingFieldMap?.has(fieldId)) {
374
+ stats.fieldsPreserved++;
375
+ } else {
376
+ stats.fieldsAdded++;
377
+ }
378
+ }
379
+
380
+ // Check for removed fields (in existing but not in fresh)
381
+ if (existingFieldMap) {
382
+ for (const [fieldId] of existingFieldMap) {
383
+ if (!seenFieldIds.has(fieldId)) {
384
+ stats.fieldsRemoved++;
385
+ }
386
+ }
387
+ }
388
+
389
+ merged.tables[tableAlias] = {
390
+ id: tableId,
391
+ fields: mergedFields,
392
+ };
393
+
394
+ if (existingTableIdToAlias.has(tableId)) {
395
+ stats.tablesPreserved++;
396
+ } else {
397
+ stats.tablesAdded++;
398
+ }
399
+ }
400
+
401
+ // Check for removed tables (in existing but not in fresh)
402
+ for (const [, table] of Object.entries(existing.tables)) {
403
+ if (!seenTableIds.has(table.id)) {
404
+ stats.tablesRemoved++;
405
+ }
406
+ }
407
+
408
+ return { merged, stats };
409
+ }
410
+
411
+ interface MergeStats {
412
+ tablesAdded: number;
413
+ tablesRemoved: number;
414
+ tablesPreserved: number;
415
+ fieldsAdded: number;
416
+ fieldsRemoved: number;
417
+ fieldsPreserved: number;
418
+ }
419
+
420
+ /**
421
+ * Main CLI entry point
422
+ */
423
+ export async function main(args: string[]): Promise<void> {
424
+ const options = parseArgs(args);
425
+
426
+ if (options.help) {
427
+ showHelp();
428
+ return;
429
+ }
430
+
431
+ // Validate required options
432
+ const realm = options.realm;
433
+ const app = options.app;
434
+ const token = options.token || process.env.QB_USER_TOKEN;
435
+
436
+ if (!realm) {
437
+ console.error('Error: --realm is required');
438
+ process.exit(1);
439
+ }
440
+
441
+ if (!app) {
442
+ console.error('Error: --app is required');
443
+ process.exit(1);
444
+ }
445
+
446
+ if (!token) {
447
+ console.error('Error: --token is required (or set QB_USER_TOKEN env var)');
448
+ process.exit(1);
449
+ }
450
+
451
+ const format = options.format || 'ts';
452
+
453
+ // Merge requires an output file
454
+ if (options.merge && !options.output) {
455
+ console.error('Error: --merge requires --output to specify the file to merge with');
456
+ process.exit(1);
457
+ }
458
+
459
+ try {
460
+ console.error(`Fetching schema from ${realm}/${app}...`);
461
+
462
+ let schema = await fetchSchema(realm, app, token);
463
+ const tableCount = Object.keys(schema.tables).length;
464
+ const fieldCount = Object.values(schema.tables).reduce(
465
+ (sum, t) => sum + Object.keys(t.fields).length,
466
+ 0
467
+ );
468
+
469
+ console.error(`Found ${tableCount} tables with ${fieldCount} fields`);
470
+
471
+ // Merge with existing schema if requested
472
+ if (options.merge && options.output) {
473
+ const existing = await loadExistingSchema(options.output, format);
474
+ if (existing) {
475
+ const { merged, stats } = mergeSchemas(existing, schema);
476
+ schema = merged;
477
+
478
+ console.error(`Merge complete:`);
479
+ console.error(` Tables: ${stats.tablesPreserved} preserved, ${stats.tablesAdded} added, ${stats.tablesRemoved} removed`);
480
+ console.error(` Fields: ${stats.fieldsPreserved} preserved, ${stats.fieldsAdded} added, ${stats.fieldsRemoved} removed`);
481
+ } else {
482
+ console.error(`No existing schema found at ${options.output}, creating new file`);
483
+ }
484
+ }
485
+
486
+ // Format output
487
+ const output = format === 'json'
488
+ ? formatAsJson(schema)
489
+ : formatAsTypeScript(schema);
490
+
491
+ // Write output
492
+ if (options.output) {
493
+ const fs = await import('fs');
494
+ fs.writeFileSync(options.output, output, 'utf-8');
495
+ console.error(`Schema written to ${options.output}`);
496
+ } else {
497
+ console.log(output);
498
+ }
499
+ } catch (error) {
500
+ console.error('Error:', error instanceof Error ? error.message : error);
501
+ process.exit(1);
502
+ }
503
+ }
504
+
505
+ // Run if executed directly
506
+ const isDirectExecution =
507
+ typeof process !== 'undefined' &&
508
+ process.argv[1]?.endsWith('schema.js');
509
+
510
+ if (isDirectExecution) {
511
+ main(process.argv.slice(2));
512
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Client module exports
3
+ */
4
+
5
+ export type { RequestOptions, RequestExecutorOptions } from './request.js';
6
+ export { executeRequest, createRequestExecutor } from './request.js';
7
+
8
+ export type { Throttle } from './throttle.js';
9
+ export { SlidingWindowThrottle, NoOpThrottle, createThrottle } from './throttle.js';
10
+
11
+ export type { RetryOptions } from './retry.js';
12
+ export {
13
+ calculateBackoffDelay,
14
+ getRetryDelay,
15
+ extractRateLimitInfo,
16
+ sleep,
17
+ isRetryableStatus,
18
+ executeWithRetry,
19
+ } from './retry.js';
20
+
21
+ export type {
22
+ PaginationOptions,
23
+ PaginationMetadata,
24
+ PaginatedResponse,
25
+ PaginationType,
26
+ RequestExecutor,
27
+ PaginatedRequestExecutor,
28
+ } from './pagination.js';
29
+ export {
30
+ PaginatedRequest,
31
+ createPaginatedRequest,
32
+ detectPaginationType,
33
+ hasMorePages,
34
+ findDataKey,
35
+ isPaginatedResponse,
36
+ } from './pagination.js';