quickbase-js 1.0.1-beta.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/build.js +66 -0
  5. package/dist/quickbaseClient.d.ts +3888 -0
  6. package/dist/quickbaseClient.js +5812 -0
  7. package/dist/quickbaseClient.js.map +1 -0
  8. package/examples/react.js +22 -0
  9. package/examples/vanillajs.html +23 -0
  10. package/logGen.yaml +1820 -0
  11. package/package.json +72 -0
  12. package/playwright.config.ts +37 -0
  13. package/rollup.config.js +70 -0
  14. package/src/code-generation/fix-spec-definitions.ts +136 -0
  15. package/src/code-generation/fix-spec-main.ts +176 -0
  16. package/src/code-generation/fix-spec-paths.ts +163 -0
  17. package/src/code-generation/generate-unified-interface.ts +152 -0
  18. package/src/code-generation/output/quickbase-fixed.json +8435 -0
  19. package/src/code-generation/regenerate-client.ts +110 -0
  20. package/src/generated-unified/QuickbaseClient.ts +59 -0
  21. package/src/index.ts +1 -0
  22. package/src/log-gen.ts +122 -0
  23. package/src/quickbaseClient.ts +320 -0
  24. package/src/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +46878 -0
  25. package/src/tokenCache.ts +51 -0
  26. package/src/utils.ts +5 -0
  27. package/test-artifacts/.last-run.json +4 -0
  28. package/test-artifacts/test-results.json +1801 -0
  29. package/test-results.json +2050 -0
  30. package/tests/playwright/qb/auth/cacheSwitching.test.ts +189 -0
  31. package/tests/playwright/qb/auth/fetchTempToken401.test.ts +217 -0
  32. package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +151 -0
  33. package/tests/playwright/qb/auth/getTempToken.test.ts +180 -0
  34. package/tests/playwright/qb/auth/tokenPrefetch.test.ts +192 -0
  35. package/tests/playwright/qb/auth/tokenRenewal.test.ts +110 -0
  36. package/tests/setup.ts +19 -0
  37. package/tests/vitest/qb/apps/getApp.test.ts +46 -0
  38. package/tests/vitest/qb/fields/getFields.test.ts +234 -0
  39. package/tests/vitest/qb/index.ts +4 -0
  40. package/tests/vitest/qb/tables/getAppTables.test.ts +67 -0
  41. package/tests/vitest/unit/apps/getApp.test.ts +81 -0
  42. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +59 -0
  43. package/tests/vitest/unit/auth/getTempToken.test.ts +83 -0
  44. package/tests/vitest/unit/auth/retryOn401.test.ts +72 -0
  45. package/tests/vitest/unit/auth/two401s.test.ts +77 -0
  46. package/tests/vitest/unit/fields/getFields.test.ts +115 -0
  47. package/tests/vitest/unit/index.ts +4 -0
  48. package/tests/vitest/unit/tables/getAppTables.test.ts +72 -0
  49. package/text.txt +104 -0
  50. package/tsconfig.json +28 -0
  51. package/versionrc.json +9 -0
  52. package/vitest.config.ts +21 -0
package/logGen.yaml ADDED
@@ -0,0 +1,1820 @@
1
+ project: quickbase-js
2
+ date: 3/9/2025
3
+ root: /home/drew/Projects/quickbase-js
4
+ goals:
5
+ - Implement a QuickBase API client in TypeScript.
6
+ - Generate client methods from the QuickBase RESTful API spec.
7
+ - Provide a simple and intuitive interface for interacting with QuickBase.
8
+ - Support both Node.js and browser environments.
9
+ - 'Use case: JS frameworks like React, Vue, and Angular hosted in QuickBase code pages.'
10
+ - Node.js is supported using User Tokens
11
+ - Use OpenAPI to generate the TypeScript types and methods.
12
+ - Use the latest ES6+ features and TypeScript features.
13
+ - '#'
14
+ - No manual updating methods when the QuickBase API changes.
15
+ - The proxy magic dynamically maps generated methods.
16
+ - No manual endpoint definitions—endpoints come from the generated apis (e.g., AppsApi.getApp).
17
+ - '#'
18
+ - 'Per-Instance Store: tokenCache is defined per quickbaseClient call, so each instance has its own isolated TokenCache.'
19
+ - 'No Singleton: No shared state—tokens are fetched and cached within the current instance.'
20
+ tree:
21
+ name: .
22
+ type: directory
23
+ children:
24
+ - name: .env
25
+ type: file
26
+ - name: .gitignore
27
+ type: file
28
+ - name: LICENSE
29
+ type: file
30
+ - name: README.md
31
+ type: file
32
+ - name: build.js
33
+ type: file
34
+ - name: dist
35
+ type: directory
36
+ children:
37
+ - name: quickbaseClient.js
38
+ type: file
39
+ - name: quickbaseClient.js.map
40
+ type: file
41
+ - name: examples
42
+ type: directory
43
+ children:
44
+ - name: react.js
45
+ type: file
46
+ - name: vanillajs.html
47
+ type: file
48
+ - name: logGen.yaml
49
+ type: file
50
+ - name: openapitools.json
51
+ type: file
52
+ - name: package-lock.json
53
+ type: file
54
+ - name: package.json
55
+ type: file
56
+ contents: |
57
+ {
58
+ "name": "quickbase-js",
59
+ "main": "dist/quickbaseClient.js",
60
+ "types": "dist/quickbaseClient.d.ts",
61
+ "version": "1.0.0",
62
+ "description": "QuickBase JavaScript client for frameworks and vanilla JS",
63
+ "module": "dist/quickbaseClient.js",
64
+ "type": "module",
65
+ "exports": {
66
+ ".": {
67
+ "import": "./dist/quickbaseClient.js",
68
+ "types": "./dist/quickbaseClient.d.ts"
69
+ }
70
+ },
71
+ "scripts": {
72
+ "fix-spec": "node --loader ts-node/esm src/code-generation/fix-spec-main.ts",
73
+ "regenerate": "node --loader ts-node/esm src/code-generation/regenerate-client.ts",
74
+ "generate-unified": "node --loader ts-node/esm src/code-generation/generate-unified-interface.ts",
75
+ "log-gen": "node --loader ts-node/esm src/log-gen.ts",
76
+ "build": "rimraf dist && rollup -c",
77
+ "build:prod": "rm -rf dist/* && NODE_ENV=production rollup -c",
78
+ "test": "vitest run tests/vitest/**/**/*.test.ts",
79
+ "test:pw:qb:all": "playwright test tests/playwright/qb --config playwright.config.ts",
80
+ "test:vitest:qb": "vitest run tests/vitest/qb/**/*.test.ts",
81
+ "test:vitest:qb:apps": "vitest run tests/vitest/qb/apps/**/*.test.ts",
82
+ "test:vitest:qb:auth": "vitest run tests/vitest/qb/auth/**/*.test.ts",
83
+ "test:vitest:qb:fields": "vitest run tests/vitest/qb/fields/**/*.test.ts",
84
+ "test:vitest:qb:tables": "vitest run tests/vitest/qb/tables/**/*.test.ts",
85
+ "test:pw:qb:auth": "playwright test tests/playwright/qb/auth",
86
+ "test::pw:qb:auth:temptoken": "playwright test tests/playwright/qb/auth/getTempToken.test.ts",
87
+ "test:pw:qb:auth:renewal": "playwright test tests/playwright/qb/auth/tokenRenewal.test.ts",
88
+ "test:pw:qb:auth:cache": "playwright test tests/playwright/qb/auth/cacheSwitching.test.ts",
89
+ "test:pw:qb:auth:temp401": "playwright test tests/playwright/qb/auth/fetchTempToken401.test.ts",
90
+ "test:pw:qb:auth:prefetch": "playwright test tests/playwright/qb/auth/tokenPrefetch.test.ts",
91
+ "test:pw:qb:auth:debug": "playwright test tests/playwright/qb/auth --headed --debug",
92
+ "test:pw:qb:auth:temp:debug": "playwright test tests/playwright/qb/auth/getTempToken.test.ts --headed --debug",
93
+ "test:pw:qb:auth:renewal:debug": "playwright test tests/playwright/qb/auth/tokenRenewal.test.ts --headed --debug --config=playwright.config.ts",
94
+ "test:pw:qb:auth:cache:debug": "playwright test tests/playwright/qb/auth/cacheSwitching.test.ts --headed --debug"
95
+ },
96
+ "dependencies": {
97
+ "typescript": "^5.8.2"
98
+ },
99
+ "devDependencies": {
100
+ "@openapitools/openapi-generator-cli": "^2.17.0",
101
+ "@playwright/test": "^1.51.0",
102
+ "@rollup/plugin-commonjs": "^28.0.3",
103
+ "@rollup/plugin-node-resolve": "^16.0.0",
104
+ "@rollup/plugin-terser": "^0.4.4",
105
+ "@rollup/plugin-typescript": "^12.1.2",
106
+ "@types/node": "^20.17.24",
107
+ "dotenv": "^16.4.7",
108
+ "glob": "^11.0.1",
109
+ "js-yaml": "^4.1.0",
110
+ "jsdom": "^24.1.3",
111
+ "node-fetch": "^3.3.2",
112
+ "openapi-types": "^12.1.3",
113
+ "rimraf": "^5.0.10",
114
+ "rollup": "^4.34.9",
115
+ "rollup-plugin-dts": "^6.1.1",
116
+ "ts-morph": "^23.0.0",
117
+ "ts-node": "^10.9.2",
118
+ "tslib": "^2.8.1",
119
+ "vitest": "^1.6.0"
120
+ }
121
+ }
122
+ - name: playwright.config.ts
123
+ type: file
124
+ - name: rollup.config.js
125
+ type: file
126
+ contents: |
127
+ import typescript from "@rollup/plugin-typescript";
128
+ import terser from "@rollup/plugin-terser";
129
+ import nodeResolve from "@rollup/plugin-node-resolve";
130
+ import commonjs from "@rollup/plugin-commonjs";
131
+ import dts from "rollup-plugin-dts";
132
+
133
+ const isProd = process.env.NODE_ENV === "production";
134
+
135
+ export default [
136
+ {
137
+ input: "src/quickbaseClient.ts",
138
+ output: {
139
+ dir: "dist",
140
+ format: "esm",
141
+ sourcemap: true,
142
+ compact: isProd,
143
+ entryFileNames: "quickbaseClient.js",
144
+ },
145
+ external: [
146
+ "node:http",
147
+ "node:https",
148
+ "node:zlib",
149
+ "node:stream",
150
+ "node:buffer",
151
+ "node:util",
152
+ "node:url",
153
+ "node:net",
154
+ "node:fs",
155
+ "node:path",
156
+ "node-fetch",
157
+ ],
158
+ plugins: [
159
+ nodeResolve({ preferBuiltins: true }),
160
+ commonjs(),
161
+ typescript({
162
+ tsconfig: "./tsconfig.json", // Updated to single tsconfig
163
+ declaration: false, // Rollup handles this separately
164
+ }),
165
+ isProd && terser(),
166
+ ],
167
+ },
168
+ {
169
+ input: "src/quickbaseClient.ts",
170
+ output: {
171
+ file: "dist/quickbaseClient.d.ts",
172
+ format: "esm",
173
+ },
174
+ plugins: [
175
+ dts({
176
+ tsconfig: "./tsconfig.json", // Updated to single tsconfig
177
+ }),
178
+ ],
179
+ },
180
+ ].map((config) => ({
181
+ ...config,
182
+ onwarn(warning, warn) {
183
+ if (
184
+ warning.code === "PLUGIN_WARNING" &&
185
+ warning.plugin === "typescript" &&
186
+ warning.message.includes("TS5096")
187
+ ) {
188
+ return;
189
+ }
190
+ warn(warning);
191
+ },
192
+ }));
193
+ - name: src
194
+ type: directory
195
+ children:
196
+ - name: code-generation
197
+ type: directory
198
+ children:
199
+ - name: fix-spec-definitions.ts
200
+ type: file
201
+ contents: |
202
+ export const definitions = {
203
+ App: {
204
+ type: "object",
205
+ properties: {
206
+ id: { type: "string" },
207
+ name: { type: "string" },
208
+ created: { type: "string", format: "date-time" },
209
+ updated: { type: "string", format: "date-time" },
210
+ description: { type: "string" },
211
+ timeZone: { type: "string" },
212
+ dateFormat: { type: "string" },
213
+ hasEveryoneOnTheInternet: { type: "boolean" },
214
+ memoryInfo: {
215
+ type: "object",
216
+ properties: {
217
+ estMemory: { type: "number" },
218
+ estMemoryInclDependentApps: { type: "number" },
219
+ },
220
+ },
221
+ securityProperties: {
222
+ type: "object",
223
+ properties: {
224
+ allowClone: { type: "boolean" },
225
+ allowExport: { type: "boolean" },
226
+ enableAppTokens: { type: "boolean" },
227
+ hideFromPublic: { type: "boolean" },
228
+ mustBeRealmApproved: { type: "boolean" },
229
+ useIPFilter: { type: "boolean" },
230
+ },
231
+ },
232
+ },
233
+ required: ["id", "name"],
234
+ },
235
+ Field: {
236
+ type: "object",
237
+ properties: {
238
+ id: { type: "number" },
239
+ label: { type: "string" },
240
+ fieldType: { type: "string" },
241
+ noWrap: { type: "boolean" },
242
+ bold: { type: "boolean" },
243
+ required: { type: "boolean" },
244
+ appearsByDefault: { type: "boolean" },
245
+ findEnabled: { type: "boolean" },
246
+ unique: { type: "boolean" },
247
+ doesDataCopy: { type: "boolean" },
248
+ fieldHelp: { type: "string" },
249
+ audited: { type: "boolean" },
250
+ properties: {
251
+ type: "object",
252
+ properties: {
253
+ primaryKey: { type: "boolean" },
254
+ foreignKey: { type: "boolean" },
255
+ numLines: { type: "number" },
256
+ maxLength: { type: "number" },
257
+ appendOnly: { type: "boolean" },
258
+ allowHTML: { type: "boolean" },
259
+ allowMentions: { type: "boolean" },
260
+ sortAsGiven: { type: "boolean" },
261
+ carryChoices: { type: "boolean" },
262
+ allowNewChoices: { type: "boolean" },
263
+ formula: { type: "string" },
264
+ defaultValue: { type: "string" },
265
+ },
266
+ },
267
+ permissions: {
268
+ type: "array",
269
+ items: {
270
+ type: "object",
271
+ properties: {
272
+ permissionType: { type: "string" },
273
+ role: { type: "string" },
274
+ roleId: { type: "number" },
275
+ },
276
+ },
277
+ },
278
+ },
279
+ required: ["id", "label", "fieldType"],
280
+ },
281
+ Table: {
282
+ type: "object",
283
+ properties: {
284
+ id: { type: "string" },
285
+ name: { type: "string" },
286
+ alias: { type: "string" },
287
+ created: { type: "string", format: "date-time" },
288
+ defaultSortFieldId: { type: "number" },
289
+ defaultSortOrder: { type: "string" },
290
+ description: { type: "string" },
291
+ keyFieldId: { type: "number" },
292
+ nextFieldId: { type: "number" },
293
+ nextRecordId: { type: "number" },
294
+ pluralRecordName: { type: "string" },
295
+ singleRecordName: { type: "string" },
296
+ sizeLimit: { type: "string" },
297
+ spaceRemaining: { type: "string" },
298
+ spaceUsed: { type: "string" },
299
+ updated: { type: "string", format: "date-time" },
300
+ },
301
+ required: ["id", "name"],
302
+ },
303
+ Record: {
304
+ type: "object",
305
+ properties: {
306
+ id: { type: "number" },
307
+ fields: {
308
+ type: "object",
309
+ additionalProperties: { type: "string" },
310
+ },
311
+ },
312
+ required: ["id"],
313
+ },
314
+ Upsert200Response: {
315
+ type: "object",
316
+ properties: {
317
+ id: { type: "number" },
318
+ status: { type: "string" },
319
+ },
320
+ required: ["id"],
321
+ },
322
+ ReportRunResponse: {
323
+ type: "object",
324
+ properties: {
325
+ id: { type: "string" },
326
+ data: { type: "object" }, // Placeholder
327
+ },
328
+ required: ["id"],
329
+ },
330
+ GetTempTokenDBID200Response: {
331
+ type: "object",
332
+ properties: {
333
+ temporaryAuthorization: { type: "string" },
334
+ },
335
+ required: ["temporaryAuthorization"],
336
+ },
337
+ };
338
+ - name: fix-spec-main.ts
339
+ type: file
340
+ contents: |
341
+ #!/usr/bin/env node
342
+ import { promises as fs } from "fs";
343
+ import path from "path";
344
+ import { fileURLToPath } from "url";
345
+ import * as glob from "glob";
346
+ import { paths } from "./fix-spec-paths.ts";
347
+ import { definitions } from "./fix-spec-definitions.ts";
348
+
349
+ interface Parameter {
350
+ name: string;
351
+ in: string;
352
+ required?: boolean;
353
+ type?: string;
354
+ schema?: { type?: string; items?: any; $ref?: string; properties?: any };
355
+ example?: any;
356
+ }
357
+
358
+ interface Operation {
359
+ parameters?: Parameter[];
360
+ responses?: Record<string, { description: string; schema?: any }>;
361
+ operationId?: string;
362
+ summary?: string;
363
+ tags?: string[];
364
+ }
365
+
366
+ interface Spec {
367
+ paths: Record<string, Record<string, Operation>>;
368
+ definitions?: Record<string, any>;
369
+ operations?: any;
370
+ groups?: any;
371
+ components?: any;
372
+ }
373
+
374
+ function toCamelCase(str: string): string {
375
+ return str
376
+ .replace(/[-_](.)/g, (_, char) => char.toUpperCase())
377
+ .replace(/^./, (str) => str.toLowerCase());
378
+ }
379
+
380
+ function fixArraySchemas(spec: Spec) {
381
+ for (const pathKey in spec.paths) {
382
+ for (const method in spec.paths[pathKey]) {
383
+ const operation = spec.paths[pathKey][method];
384
+ if (operation.parameters) {
385
+ operation.parameters.forEach((param: Parameter) => {
386
+ if (param.schema) {
387
+ if (param.schema.type === "array" && !param.schema.items) {
388
+ console.log(
389
+ `Fixing missing items in ${pathKey}(${method}).${param.name}`
390
+ );
391
+ param.schema.items =
392
+ pathKey === "/records" && param.name === "generated"
393
+ ? { $ref: "#/definitions/Record" }
394
+ : { type: "string" };
395
+ }
396
+ if (param.schema.properties) {
397
+ for (const propKey in param.schema.properties) {
398
+ const prop = param.schema.properties[propKey];
399
+ if (prop.type === "array" && !prop.items) {
400
+ console.log(
401
+ `Fixing nested array in ${pathKey}(${method}).${param.name}.${propKey}`
402
+ );
403
+ prop.items =
404
+ propKey === "data" && pathKey === "/records"
405
+ ? { $ref: "#/definitions/Record" }
406
+ : { type: "string" };
407
+ }
408
+ }
409
+ }
410
+ }
411
+ });
412
+ }
413
+ if (operation.responses) {
414
+ for (const status in operation.responses) {
415
+ const response = operation.responses[status];
416
+ if (response.schema) {
417
+ if (response.schema.type === "array" && !response.schema.items) {
418
+ console.log(
419
+ `Fixing missing items in ${pathKey}(${method}).responses.${status}`
420
+ );
421
+ response.schema.items =
422
+ pathKey === "/records"
423
+ ? { $ref: "#/definitions/Upsert200Response" }
424
+ : { type: "string" };
425
+ }
426
+ if (response.schema.properties) {
427
+ for (const propKey in response.schema.properties) {
428
+ const prop = response.schema.properties[propKey];
429
+ if (prop.type === "array" && !prop.items) {
430
+ console.log(
431
+ `Fixing nested array in ${pathKey}(${method}).responses.${status}.${propKey}`
432
+ );
433
+ prop.items =
434
+ propKey === "data" && pathKey === "/records"
435
+ ? { $ref: "#/definitions/Upsert200Response" }
436
+ : { type: "string" };
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ async function fixQuickBaseSpec(): Promise<void> {
448
+ try {
449
+ const CODEGEN_DIR = path.dirname(fileURLToPath(import.meta.url));
450
+ const SPECS_DIR = path.join(CODEGEN_DIR, "..", "specs");
451
+ const OUTPUT_DIR = path.join(CODEGEN_DIR, "output");
452
+ console.log("Finding latest QuickBase RESTful API spec...");
453
+ const specFiles = glob.sync(
454
+ path.join(SPECS_DIR, "QuickBase_RESTful_*.json")
455
+ );
456
+ if (specFiles.length === 0) {
457
+ console.error(
458
+ "No QuickBase_RESTful_*.json files found in specs/ folder."
459
+ );
460
+ process.exit(1);
461
+ }
462
+ const inputFile = specFiles.sort().pop() as string;
463
+ const outputFile = path.join(OUTPUT_DIR, "quickbase-fixed.json");
464
+
465
+ console.log(`Reading ${path.basename(inputFile)} from specs/...`);
466
+ const specContent = await fs.readFile(inputFile, "utf8");
467
+ const spec: Spec = JSON.parse(specContent);
468
+
469
+ console.log("Fixing parameters...");
470
+ for (const pathKey in spec.paths) {
471
+ for (const method in spec.paths[pathKey]) {
472
+ const operation = spec.paths[pathKey][method];
473
+ if (operation.parameters) {
474
+ operation.parameters = operation.parameters
475
+ .filter(
476
+ (param) =>
477
+ !["QB-Realm-Hostname", "Authorization", "User-Agent"].includes(
478
+ param.name
479
+ )
480
+ )
481
+ .map((param: Parameter) => {
482
+ param.name = toCamelCase(param.name);
483
+ if ("example" in param) delete param.example;
484
+ if ("schema" in param && param.in !== "body") delete param.schema;
485
+ if (!param.type && param.in !== "body") param.type = "string";
486
+ return param;
487
+ });
488
+ }
489
+ }
490
+ }
491
+
492
+ console.log("Applying endpoint fixes...");
493
+ spec.paths = { ...spec.paths, ...paths }; // Merge original and custom paths
494
+
495
+ console.log("Fixing array schemas...");
496
+ fixArraySchemas(spec);
497
+
498
+ console.log("Applying definitions...");
499
+ spec.definitions = { ...spec.definitions, ...definitions }; // Merge definitions
500
+
501
+ console.log("Removing unexpected top-level attributes...");
502
+ delete spec.operations;
503
+ delete spec.groups;
504
+ delete spec.components;
505
+
506
+ console.log(`Writing fixed spec to ${path.basename(outputFile)}...`);
507
+ await fs.mkdir(OUTPUT_DIR, { recursive: true });
508
+ await fs.writeFile(outputFile, JSON.stringify(spec, null, 2), "utf8");
509
+ console.log("Spec fixed successfully!");
510
+ } catch (error) {
511
+ console.error("Failed to fix spec:", error);
512
+ process.exit(1);
513
+ }
514
+ }
515
+
516
+ fixQuickBaseSpec();
517
+ - name: fix-spec-paths.ts
518
+ type: file
519
+ contents: |
520
+ export const paths = {
521
+ "/apps/{appId}": {
522
+ get: {
523
+ operationId: "getApp",
524
+ summary: "Get an app by ID",
525
+ tags: ["Apps"],
526
+ parameters: [
527
+ {
528
+ name: "appId",
529
+ in: "path",
530
+ required: true,
531
+ type: "string",
532
+ },
533
+ ],
534
+ responses: {
535
+ 200: {
536
+ description: "Success",
537
+ schema: { $ref: "#/definitions/App" },
538
+ },
539
+ },
540
+ },
541
+ },
542
+ "/fields": {
543
+ get: {
544
+ operationId: "getFields",
545
+ summary: "Get all fields for a table",
546
+ tags: ["Fields"],
547
+ parameters: [
548
+ {
549
+ name: "tableId",
550
+ in: "query",
551
+ required: true,
552
+ type: "string",
553
+ },
554
+ {
555
+ name: "includeFieldPerms",
556
+ in: "query",
557
+ required: false,
558
+ type: "boolean",
559
+ },
560
+ ],
561
+ responses: {
562
+ 200: {
563
+ description: "Success",
564
+ schema: {
565
+ type: "array",
566
+ items: { $ref: "#/definitions/Field" },
567
+ },
568
+ },
569
+ },
570
+ },
571
+ },
572
+ "/tables": {
573
+ get: {
574
+ operationId: "getAppTables",
575
+ summary: "Get all tables for an app",
576
+ tags: ["Tables"],
577
+ parameters: [
578
+ {
579
+ name: "appId",
580
+ in: "query",
581
+ required: true,
582
+ type: "string",
583
+ },
584
+ ],
585
+ responses: {
586
+ 200: {
587
+ description: "Success",
588
+ schema: {
589
+ type: "array",
590
+ items: { $ref: "#/definitions/Table" },
591
+ },
592
+ },
593
+ },
594
+ },
595
+ },
596
+ "/records/query": {
597
+ post: {
598
+ operationId: "upsertRecords",
599
+ summary: "Upsert records",
600
+ tags: ["Records"],
601
+ parameters: [
602
+ {
603
+ name: "generated",
604
+ in: "body",
605
+ required: true,
606
+ schema: {
607
+ type: "array",
608
+ items: { $ref: "#/definitions/Record" },
609
+ },
610
+ },
611
+ ],
612
+ responses: {
613
+ 200: {
614
+ description: "Success",
615
+ schema: {
616
+ type: "array",
617
+ items: { $ref: "#/definitions/Upsert200Response" },
618
+ },
619
+ },
620
+ 207: {
621
+ description: "Multi-Status (partial success)",
622
+ schema: {
623
+ type: "array",
624
+ items: { $ref: "#/definitions/Upsert200Response" },
625
+ },
626
+ },
627
+ },
628
+ },
629
+ },
630
+ "/reports/{reportId}/run": {
631
+ post: {
632
+ operationId: "runReport",
633
+ summary: "Run a report",
634
+ tags: ["Reports"],
635
+ parameters: [
636
+ {
637
+ name: "reportId",
638
+ in: "path",
639
+ required: true,
640
+ type: "string",
641
+ },
642
+ {
643
+ name: "generated",
644
+ in: "body",
645
+ required: true,
646
+ schema: { type: "object" },
647
+ },
648
+ ],
649
+ responses: {
650
+ 200: {
651
+ description: "Success",
652
+ schema: {
653
+ type: "array",
654
+ items: { $ref: "#/definitions/ReportRunResponse" },
655
+ },
656
+ },
657
+ },
658
+ },
659
+ },
660
+ "/auth/temporary/{dbid}": {
661
+ get: {
662
+ operationId: "getTempTokenDBID",
663
+ summary: "Get a temporary authorization token for a specific dbid",
664
+ tags: ["Auth"],
665
+ parameters: [
666
+ {
667
+ name: "dbid",
668
+ in: "path",
669
+ required: true,
670
+ type: "string",
671
+ },
672
+ ],
673
+ responses: {
674
+ 200: {
675
+ description: "Success",
676
+ schema: { $ref: "#/definitions/GetTempTokenDBID200Response" },
677
+ },
678
+ },
679
+ },
680
+ },
681
+ };
682
+ - name: generate-unified-interface.ts
683
+ type: file
684
+ contents: |
685
+ // src/code-generation/generate-unified-interface.ts
686
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
687
+ import { join, dirname } from "path";
688
+ import { fileURLToPath } from "url";
689
+ import { simplifyName } from "../utils.ts";
690
+ import { OpenAPIV2 } from "openapi-types";
691
+
692
+ const __dirname = dirname(fileURLToPath(import.meta.url));
693
+ const SPEC_FILE = join(__dirname, "output", "quickbase-fixed.json");
694
+ const OUTPUT_DIR = join(__dirname, "..", "generated-unified");
695
+ const OUTPUT_FILE = join(OUTPUT_DIR, "QuickbaseClient.ts");
696
+
697
+ function generateInterface() {
698
+ if (!existsSync(SPEC_FILE)) {
699
+ console.error(
700
+ `Spec file ${SPEC_FILE} not found. Run 'npm run fix-spec' first.`
701
+ );
702
+ process.exit(1);
703
+ }
704
+
705
+ console.log("Generating unified QuickbaseClient interface...");
706
+ const spec = JSON.parse(
707
+ readFileSync(SPEC_FILE, "utf8")
708
+ ) as OpenAPIV2.Document;
709
+ const { paths } = spec;
710
+
711
+ const modelImports = new Set<string>();
712
+ const methods: string[] = [];
713
+
714
+ for (const [path, methodsObj] of Object.entries(
715
+ paths as OpenAPIV2.PathsObject
716
+ )) {
717
+ if (!methodsObj) continue;
718
+
719
+ for (const [method, operation] of Object.entries(
720
+ methodsObj as OpenAPIV2.PathItemObject
721
+ )) {
722
+ const op = operation as OpenAPIV2.OperationObject | undefined;
723
+ if (!op || !op.operationId) continue;
724
+
725
+ const opId = op.operationId; // Use operationId directly, no simplification
726
+ const params = (op.parameters || [])
727
+ .filter((p) => {
728
+ const param = p as OpenAPIV2.ParameterObject;
729
+ return !["QB-Realm-Hostname", "Authorization", "User-Agent"].includes(
730
+ param.name
731
+ );
732
+ })
733
+ .map((p) => {
734
+ const param = p as OpenAPIV2.ParameterObject;
735
+ const type = param.type
736
+ ? mapOpenApiTypeToTs(param.type)
737
+ : param.schema
738
+ ? mapRefToType(param.schema, modelImports)
739
+ : "any";
740
+ return `${param.name}${param.required ? "" : "?"}: ${type}`;
741
+ })
742
+ .join("; ");
743
+ const response = op.responses?.["200"] as
744
+ | OpenAPIV2.ResponseObject
745
+ | undefined;
746
+ const returnType = response?.schema
747
+ ? mapRefToType(response.schema, modelImports)
748
+ : "void";
749
+ methods.push(
750
+ ` ${opId}: (params: { ${params} }) => Promise<${returnType}>;`
751
+ );
752
+ }
753
+ }
754
+
755
+ // Generate import and export statements for all model types
756
+ const importExportLines = Array.from(modelImports)
757
+ .map((m) => `export { ${m} } from "../generated/models/${m}.ts";`)
758
+ .join("\n");
759
+
760
+ const interfaceContent = `// Generated on ${new Date().toISOString()}\n${importExportLines}\n\nexport interface QuickbaseClient {\n${methods.join(
761
+ "\n"
762
+ )}\n}\n`;
763
+
764
+ if (!existsSync(OUTPUT_DIR)) {
765
+ mkdirSync(OUTPUT_DIR, { recursive: true });
766
+ }
767
+ writeFileSync(OUTPUT_FILE, interfaceContent, "utf8");
768
+ console.log(`Generated ${OUTPUT_FILE}`);
769
+ }
770
+
771
+ function mapOpenApiTypeToTs(openApiType: string): string {
772
+ switch (openApiType.toLowerCase()) {
773
+ case "integer":
774
+ case "int":
775
+ return "number";
776
+ case "string":
777
+ return "string";
778
+ case "boolean":
779
+ return "boolean";
780
+ default:
781
+ return "any";
782
+ }
783
+ }
784
+
785
+ function mapRefToType(
786
+ schema: OpenAPIV2.SchemaObject | OpenAPIV2.ReferenceObject,
787
+ modelImports: Set<string>
788
+ ): string {
789
+ if ("$ref" in schema) {
790
+ const model = schema.$ref.split("/").pop()!;
791
+ modelImports.add(model);
792
+ return model;
793
+ }
794
+ if (schema.type === "array" && schema.items) {
795
+ const itemType =
796
+ "$ref" in schema.items
797
+ ? schema.items.$ref.split("/").pop()!
798
+ : mapOpenApiTypeToTs(schema.items.type || "any");
799
+ if ("$ref" in schema.items) modelImports.add(itemType);
800
+ return `${itemType}[]`;
801
+ }
802
+ return mapOpenApiTypeToTs(schema.type || "any");
803
+ }
804
+
805
+ try {
806
+ generateInterface();
807
+ } catch (error) {
808
+ console.error("Generation failed:", error);
809
+ process.exit(1);
810
+ }
811
+ - name: output
812
+ type: directory
813
+ children:
814
+ - name: quickbase-fixed.json
815
+ type: file
816
+ - name: regenerate-client.ts
817
+ type: file
818
+ contents: |
819
+ #!/usr/bin/env node
820
+ import { execSync } from "child_process";
821
+ import { existsSync } from "fs";
822
+ import { join } from "path";
823
+ import { fileURLToPath } from "url";
824
+ import fetch from "node-fetch";
825
+ import readline from "readline";
826
+
827
+ const CURRENT_JAR_VERSION = "7.12.0";
828
+ const MAVEN_METADATA_URL =
829
+ "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/maven-metadata.xml";
830
+ const CODEGEN_DIR = join(fileURLToPath(import.meta.url), "..");
831
+ const JARS_DIR = join(CODEGEN_DIR, "..", "openapi-jars");
832
+ const SPEC_INPUT = join(CODEGEN_DIR, "output", "quickbase-fixed.json"); // Updated path
833
+ const OUTPUT_DIR = join(CODEGEN_DIR, "..", "generated");
834
+
835
+ async function getLatestVersion(): Promise<string> {
836
+ const response = await fetch(MAVEN_METADATA_URL);
837
+ if (!response.ok)
838
+ throw new Error(`Failed to fetch Maven metadata: ${response.statusText}`);
839
+ const text = await response.text();
840
+ const match = text.match(/<latest>(.*?)<\/latest>/);
841
+ if (!match)
842
+ throw new Error("Couldn’t parse latest version from Maven metadata");
843
+ return match[1];
844
+ }
845
+
846
+ async function checkAndPromptForUpdate(messages: string[]): Promise<string> {
847
+ const latestVersion = await getLatestVersion();
848
+ if (latestVersion === CURRENT_JAR_VERSION) {
849
+ messages.push(`Current version (${CURRENT_JAR_VERSION}) is the latest.`);
850
+ return CURRENT_JAR_VERSION;
851
+ }
852
+
853
+ messages.push(
854
+ `Newer version available: ${latestVersion} (current: ${CURRENT_JAR_VERSION})`
855
+ );
856
+ const rl = readline.createInterface({
857
+ input: process.stdin,
858
+ output: process.stdout,
859
+ });
860
+
861
+ return new Promise((resolve) => {
862
+ const prompt = "Do you want to update to the latest version? (y/n): ";
863
+ messages.push(prompt);
864
+ rl.question(prompt, (answer) => {
865
+ rl.close();
866
+ if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
867
+ messages.push(`Switching to version ${latestVersion}...`);
868
+ resolve(latestVersion);
869
+ } else {
870
+ messages.push(`Sticking with version ${CURRENT_JAR_VERSION}.`);
871
+ resolve(CURRENT_JAR_VERSION);
872
+ }
873
+ });
874
+ });
875
+ }
876
+
877
+ async function ensureJarExists(
878
+ version: string,
879
+ messages: string[]
880
+ ): Promise<string> {
881
+ const jarPath = join(JARS_DIR, `openapi-generator-cli-${version}.jar`);
882
+ const jarUrl = `https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${version}/openapi-generator-cli-${version}.jar`;
883
+
884
+ if (!existsSync(jarPath)) {
885
+ messages.push(`Downloading OpenAPI Generator CLI v${version}...`);
886
+ try {
887
+ execSync(`curl -L -o ${jarPath} ${jarUrl}`, { stdio: "inherit" });
888
+ messages.push(`Downloaded ${jarPath}`);
889
+ } catch (error) {
890
+ messages.push("Failed to download JAR.");
891
+ throw error;
892
+ }
893
+ } else {
894
+ messages.push(`Using existing ${jarPath}`);
895
+ }
896
+ return jarPath;
897
+ }
898
+
899
+ function regenerateClient(jarPath: string, messages: string[]) {
900
+ messages.push("Regenerating client from spec...");
901
+ const command = `java -jar ${jarPath} generate -i ${SPEC_INPUT} -g typescript-fetch -o ${OUTPUT_DIR}`;
902
+ try {
903
+ execSync(command, { stdio: "inherit" });
904
+ messages.push("Client regeneration complete.");
905
+ } catch (error) {
906
+ messages.push("Regeneration failed.");
907
+ throw error;
908
+ }
909
+ }
910
+
911
+ async function main() {
912
+ const messages: string[] = [];
913
+ try {
914
+ const versionToUse = await checkAndPromptForUpdate(messages);
915
+ const jarPath = await ensureJarExists(versionToUse, messages);
916
+ regenerateClient(jarPath, messages);
917
+ } catch (error) {
918
+ console.error("Error occurred during process:", error);
919
+ } finally {
920
+ console.log("\n--- Key Messages ---");
921
+ messages.forEach((msg) => console.log(msg));
922
+ }
923
+ }
924
+
925
+ main().catch((error) => {
926
+ console.error("Error in regeneration process:", error);
927
+ process.exit(1);
928
+ });
929
+ - name: generated
930
+ type: directory
931
+ children:
932
+ - name: .openapi-generator
933
+ type: directory
934
+ children:
935
+ - name: FILES
936
+ type: file
937
+ - name: VERSION
938
+ type: file
939
+ - name: .openapi-generator-ignore
940
+ type: file
941
+ - name: apis
942
+ type: directory
943
+ children:
944
+ - name: AppsApi.ts
945
+ type: file
946
+ - name: AuditApi.ts
947
+ type: file
948
+ - name: AuthApi.ts
949
+ type: file
950
+ - name: DocumentTemplatesApi.ts
951
+ type: file
952
+ - name: FieldsApi.ts
953
+ type: file
954
+ - name: FilesApi.ts
955
+ type: file
956
+ - name: FormulasApi.ts
957
+ type: file
958
+ - name: PlatformAnalyticsApi.ts
959
+ type: file
960
+ - name: RecordsApi.ts
961
+ type: file
962
+ - name: ReportsApi.ts
963
+ type: file
964
+ - name: SolutionsApi.ts
965
+ type: file
966
+ - name: TablesApi.ts
967
+ type: file
968
+ - name: UserTokenApi.ts
969
+ type: file
970
+ - name: UsersApi.ts
971
+ type: file
972
+ - name: index.ts
973
+ type: file
974
+ - name: index.ts
975
+ type: file
976
+ - name: models
977
+ type: directory
978
+ children:
979
+ - name: AddMembersToGroup200Response.ts
980
+ type: file
981
+ - name: AddSubgroupsToGroup200Response.ts
982
+ type: file
983
+ - name: App.ts
984
+ type: file
985
+ - name: AppMemoryInfo.ts
986
+ type: file
987
+ - name: AppSecurityProperties.ts
988
+ type: file
989
+ - name: Audit200Response.ts
990
+ type: file
991
+ - name: Audit202Response.ts
992
+ type: file
993
+ - name: AuditRequest.ts
994
+ type: file
995
+ - name: CloneUserToken200Response.ts
996
+ type: file
997
+ - name: CloneUserTokenRequest.ts
998
+ type: file
999
+ - name: CopyApp200Response.ts
1000
+ type: file
1001
+ - name: CopyAppRequest.ts
1002
+ type: file
1003
+ - name: CreateApp200Response.ts
1004
+ type: file
1005
+ - name: CreateAppRequest.ts
1006
+ type: file
1007
+ - name: CreateRelationship200Response.ts
1008
+ type: file
1009
+ - name: CreateRelationshipRequest.ts
1010
+ type: file
1011
+ - name: DeactivateUserToken200Response.ts
1012
+ type: file
1013
+ - name: DeleteFile200Response.ts
1014
+ type: file
1015
+ - name: DeleteRecords200Response.ts
1016
+ type: file
1017
+ - name: DeleteRecordsRequest.ts
1018
+ type: file
1019
+ - name: DeleteRelationship200Response.ts
1020
+ type: file
1021
+ - name: DeleteTable200Response.ts
1022
+ type: file
1023
+ - name: DenyUsers200Response.ts
1024
+ type: file
1025
+ - name: ExchangeSsoToken200Response.ts
1026
+ type: file
1027
+ - name: ExchangeSsoTokenRequest.ts
1028
+ type: file
1029
+ - name: Field.ts
1030
+ type: file
1031
+ - name: FieldPermissionsInner.ts
1032
+ type: file
1033
+ - name: FieldProperties.ts
1034
+ type: file
1035
+ - name: GenerateDocument200Response.ts
1036
+ type: file
1037
+ - name: GetField200Response.ts
1038
+ type: file
1039
+ - name: GetRelationships200Response.ts
1040
+ type: file
1041
+ - name: GetReport200Response.ts
1042
+ type: file
1043
+ - name: GetTable200Response.ts
1044
+ type: file
1045
+ - name: GetTempTokenDBID200Response.ts
1046
+ type: file
1047
+ - name: GetUsers200Response.ts
1048
+ type: file
1049
+ - name: GetUsersRequest.ts
1050
+ type: file
1051
+ - name: PlatformAnalyticEventSummaries200Response.ts
1052
+ type: file
1053
+ - name: PlatformAnalyticEventSummaries200ResponseMetadata.ts
1054
+ type: file
1055
+ - name: PlatformAnalyticEventSummaries200ResponseResultsInner.ts
1056
+ type: file
1057
+ - name: PlatformAnalyticEventSummaries200ResponseResultsInnerEventTypesInner.ts
1058
+ type: file
1059
+ - name: PlatformAnalyticEventSummaries200ResponseResultsInnerTotals.ts
1060
+ type: file
1061
+ - name: PlatformAnalyticEventSummaries200ResponseTotals.ts
1062
+ type: file
1063
+ - name: PlatformAnalyticEventSummaries200ResponseWhereInner.ts
1064
+ type: file
1065
+ - name: PlatformAnalyticEventSummariesRequest.ts
1066
+ type: file
1067
+ - name: PlatformAnalyticEventSummariesRequestWhereInner.ts
1068
+ type: file
1069
+ - name: PlatformAnalyticReads200Response.ts
1070
+ type: file
1071
+ - name: Record.ts
1072
+ type: file
1073
+ - name: RemoveMembersFromGroup200Response.ts
1074
+ type: file
1075
+ - name: RemoveSubgroupsFromGroup200Response.ts
1076
+ type: file
1077
+ - name: ReportRunResponse.ts
1078
+ type: file
1079
+ - name: RunFormula200Response.ts
1080
+ type: file
1081
+ - name: RunFormulaRequest.ts
1082
+ type: file
1083
+ - name: Table.ts
1084
+ type: file
1085
+ - name: TransferUserToken200Response.ts
1086
+ type: file
1087
+ - name: TransferUserTokenRequest.ts
1088
+ type: file
1089
+ - name: UndenyUsers200Response.ts
1090
+ type: file
1091
+ - name: UpdateFieldRequest.ts
1092
+ type: file
1093
+ - name: UpdateRelationshipRequest.ts
1094
+ type: file
1095
+ - name: UpdateTableRequest.ts
1096
+ type: file
1097
+ - name: Upsert200Response.ts
1098
+ type: file
1099
+ - name: Upsert207Response.ts
1100
+ type: file
1101
+ - name: Upsert400Response.ts
1102
+ type: file
1103
+ - name: UpsertRequest.ts
1104
+ type: file
1105
+ - name: index.ts
1106
+ type: file
1107
+ - name: runtime.ts
1108
+ type: file
1109
+ - name: generated-old
1110
+ type: directory
1111
+ children:
1112
+ - name: .openapi-generator
1113
+ type: directory
1114
+ children:
1115
+ - name: FILES
1116
+ type: file
1117
+ - name: VERSION
1118
+ type: file
1119
+ - name: .openapi-generator-ignore
1120
+ type: file
1121
+ - name: apis
1122
+ type: directory
1123
+ children:
1124
+ - name: AppsApi.ts
1125
+ type: file
1126
+ - name: AuditApi.ts
1127
+ type: file
1128
+ - name: AuthApi.ts
1129
+ type: file
1130
+ - name: DocumentTemplatesApi.ts
1131
+ type: file
1132
+ - name: FieldsApi.ts
1133
+ type: file
1134
+ - name: FilesApi.ts
1135
+ type: file
1136
+ - name: FormulasApi.ts
1137
+ type: file
1138
+ - name: PlatformAnalyticsApi.ts
1139
+ type: file
1140
+ - name: RecordsApi.ts
1141
+ type: file
1142
+ - name: ReportsApi.ts
1143
+ type: file
1144
+ - name: SolutionsApi.ts
1145
+ type: file
1146
+ - name: TablesApi.ts
1147
+ type: file
1148
+ - name: UserTokenApi.ts
1149
+ type: file
1150
+ - name: UsersApi.ts
1151
+ type: file
1152
+ - name: index.ts
1153
+ type: file
1154
+ - name: index.ts
1155
+ type: file
1156
+ - name: models
1157
+ type: directory
1158
+ children:
1159
+ - name: AddMembersToGroup200Response.ts
1160
+ type: file
1161
+ - name: AddSubgroupsToGroup200Response.ts
1162
+ type: file
1163
+ - name: App.ts
1164
+ type: file
1165
+ - name: AppMemoryInfo.ts
1166
+ type: file
1167
+ - name: AppSecurityProperties.ts
1168
+ type: file
1169
+ - name: Audit200Response.ts
1170
+ type: file
1171
+ - name: Audit202Response.ts
1172
+ type: file
1173
+ - name: AuditRequest.ts
1174
+ type: file
1175
+ - name: CloneUserToken200Response.ts
1176
+ type: file
1177
+ - name: CloneUserTokenRequest.ts
1178
+ type: file
1179
+ - name: CopyApp200Response.ts
1180
+ type: file
1181
+ - name: CopyAppRequest.ts
1182
+ type: file
1183
+ - name: CreateApp200Response.ts
1184
+ type: file
1185
+ - name: CreateAppRequest.ts
1186
+ type: file
1187
+ - name: CreateField200Response.ts
1188
+ type: file
1189
+ - name: CreateRelationship200Response.ts
1190
+ type: file
1191
+ - name: CreateRelationshipRequest.ts
1192
+ type: file
1193
+ - name: DeactivateUserToken200Response.ts
1194
+ type: file
1195
+ - name: DeleteApp200Response.ts
1196
+ type: file
1197
+ - name: DeleteAppRequest.ts
1198
+ type: file
1199
+ - name: DeleteFields200Response.ts
1200
+ type: file
1201
+ - name: DeleteFile200Response.ts
1202
+ type: file
1203
+ - name: DeleteRelationship200Response.ts
1204
+ type: file
1205
+ - name: DenyUsers200Response.ts
1206
+ type: file
1207
+ - name: ExchangeSsoToken200Response.ts
1208
+ type: file
1209
+ - name: ExchangeSsoTokenRequest.ts
1210
+ type: file
1211
+ - name: Field.ts
1212
+ type: file
1213
+ - name: FieldPermissionsInner.ts
1214
+ type: file
1215
+ - name: FieldProperties.ts
1216
+ type: file
1217
+ - name: GenerateDocument200Response.ts
1218
+ type: file
1219
+ - name: GetField200Response.ts
1220
+ type: file
1221
+ - name: GetRelationships200Response.ts
1222
+ type: file
1223
+ - name: GetReport200Response.ts
1224
+ type: file
1225
+ - name: GetTempTokenDBID200Response.ts
1226
+ type: file
1227
+ - name: GetUsers200Response.ts
1228
+ type: file
1229
+ - name: GetUsersRequest.ts
1230
+ type: file
1231
+ - name: PlatformAnalyticEventSummaries200Response.ts
1232
+ type: file
1233
+ - name: PlatformAnalyticEventSummaries200ResponseMetadata.ts
1234
+ type: file
1235
+ - name: PlatformAnalyticEventSummaries200ResponseResultsInner.ts
1236
+ type: file
1237
+ - name: PlatformAnalyticEventSummaries200ResponseResultsInnerEventTypesInner.ts
1238
+ type: file
1239
+ - name: PlatformAnalyticEventSummaries200ResponseResultsInnerTotals.ts
1240
+ type: file
1241
+ - name: PlatformAnalyticEventSummaries200ResponseTotals.ts
1242
+ type: file
1243
+ - name: PlatformAnalyticEventSummaries200ResponseWhereInner.ts
1244
+ type: file
1245
+ - name: PlatformAnalyticEventSummariesRequest.ts
1246
+ type: file
1247
+ - name: PlatformAnalyticEventSummariesRequestWhereInner.ts
1248
+ type: file
1249
+ - name: PlatformAnalyticReads200Response.ts
1250
+ type: file
1251
+ - name: RemoveMembersFromGroup200Response.ts
1252
+ type: file
1253
+ - name: RemoveSubgroupsFromGroup200Response.ts
1254
+ type: file
1255
+ - name: RunFormula200Response.ts
1256
+ type: file
1257
+ - name: RunFormulaRequest.ts
1258
+ type: file
1259
+ - name: RunQuery200Response.ts
1260
+ type: file
1261
+ - name: RunQueryRequest.ts
1262
+ type: file
1263
+ - name: RunReport200Response.ts
1264
+ type: file
1265
+ - name: Table.ts
1266
+ type: file
1267
+ - name: TransferUserToken200Response.ts
1268
+ type: file
1269
+ - name: TransferUserTokenRequest.ts
1270
+ type: file
1271
+ - name: UndenyUsers200Response.ts
1272
+ type: file
1273
+ - name: UpdateApp200Response.ts
1274
+ type: file
1275
+ - name: UpdateAppRequest.ts
1276
+ type: file
1277
+ - name: UpdateFieldRequest.ts
1278
+ type: file
1279
+ - name: UpdateRelationshipRequest.ts
1280
+ type: file
1281
+ - name: Upsert200Response.ts
1282
+ type: file
1283
+ - name: index.ts
1284
+ type: file
1285
+ - name: runtime.ts
1286
+ type: file
1287
+ - name: generated-unified
1288
+ type: directory
1289
+ children:
1290
+ - name: QuickbaseClient.ts
1291
+ type: file
1292
+ - name: index.ts
1293
+ type: file
1294
+ - name: log-gen.ts
1295
+ type: file
1296
+ - name: openapi-jars
1297
+ type: directory
1298
+ children:
1299
+ - name: openapi-generator-cli-7.12.0.jar
1300
+ type: file
1301
+ - name: quickbaseClient.ts
1302
+ type: file
1303
+ contents: |
1304
+ // src/quickbaseClient.ts
1305
+ import { QuickbaseClient } from "./generated-unified/QuickbaseClient.ts";
1306
+ import {
1307
+ Configuration,
1308
+ HTTPHeaders,
1309
+ ResponseError,
1310
+ } from "./generated/runtime.ts";
1311
+ import * as apis from "./generated/apis/index.ts";
1312
+ import { simplifyName } from "./utils.ts";
1313
+ import { TokenCache } from "./tokenCache.ts";
1314
+
1315
+ export interface QuickbaseConfig {
1316
+ realm: string;
1317
+ userToken?: string;
1318
+ tempToken?: string;
1319
+ useTempTokens?: boolean;
1320
+ debug?: boolean;
1321
+ fetchApi?: typeof fetch;
1322
+ }
1323
+
1324
+ export interface TempTokenParams {
1325
+ appId?: string;
1326
+ tableId?: string;
1327
+ dbid?: string;
1328
+ }
1329
+
1330
+ type ApiMethod<K extends keyof QuickbaseClient> = (
1331
+ requestParameters: Parameters<QuickbaseClient[K]>[0],
1332
+ initOverrides?: RequestInit
1333
+ ) => Promise<ReturnType<QuickbaseClient[K]>>;
1334
+
1335
+ interface MethodInfo<K extends keyof QuickbaseClient> {
1336
+ api: any;
1337
+ method: ApiMethod<K>;
1338
+ paramMap: string[];
1339
+ }
1340
+
1341
+ type MethodMap = { [K in keyof QuickbaseClient]: MethodInfo<K> };
1342
+
1343
+ const getParamNames = (fn: (...args: any[]) => any): string[] =>
1344
+ fn
1345
+ .toString()
1346
+ .slice(fn.toString().indexOf("(") + 1, fn.toString().indexOf(")"))
1347
+ .split(",")
1348
+ .map((p) => p.trim().split("=")[0].trim())
1349
+ .filter((p) => p && !p.match(/^\{/) && p !== "options");
1350
+
1351
+ // New utility function to extract dbid
1352
+ const extractDbid = (
1353
+ params: Partial<TempTokenParams>,
1354
+ errorMessage: string
1355
+ ): string => {
1356
+ const dbid = params.appId || params.tableId || params.dbid;
1357
+ if (!dbid) {
1358
+ throw new Error(errorMessage);
1359
+ }
1360
+ return dbid;
1361
+ };
1362
+
1363
+ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
1364
+ const {
1365
+ realm,
1366
+ userToken,
1367
+ tempToken: initialTempToken,
1368
+ useTempTokens,
1369
+ fetchApi,
1370
+ debug,
1371
+ } = config;
1372
+ const baseUrl = `https://api.quickbase.com/v1`;
1373
+
1374
+ const tokenCache = new TokenCache();
1375
+
1376
+ const headers: HTTPHeaders = {
1377
+ "QB-Realm-Hostname": `${realm}.quickbase.com`,
1378
+ "Content-Type": "application/json",
1379
+ };
1380
+
1381
+ if (initialTempToken) {
1382
+ headers["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
1383
+ } else if (userToken && !useTempTokens) {
1384
+ headers["Authorization"] = `QB-USER-TOKEN ${userToken}`;
1385
+ }
1386
+
1387
+ const defaultFetch =
1388
+ typeof window !== "undefined" ? window.fetch.bind(window) : undefined;
1389
+ const configuration = new Configuration({
1390
+ basePath: baseUrl,
1391
+ headers: { ...headers },
1392
+ fetchApi: fetchApi || defaultFetch,
1393
+ credentials: useTempTokens ? "include" : "omit",
1394
+ });
1395
+
1396
+ if (!configuration.fetchApi && typeof window === "undefined") {
1397
+ throw new Error(
1398
+ "fetchApi must be provided in non-browser environments (e.g., Node.js)"
1399
+ );
1400
+ }
1401
+
1402
+ const apiInstances = Object.fromEntries(
1403
+ Object.entries(apis)
1404
+ .filter(([name]) => name.endsWith("Api"))
1405
+ .map(([name, ApiClass]) => [
1406
+ name.replace("Api", "").toLowerCase(),
1407
+ new ApiClass(configuration),
1408
+ ])
1409
+ );
1410
+
1411
+ const methodMap = buildMethodMap();
1412
+
1413
+ function buildMethodMap(): MethodMap {
1414
+ const methodMap: Partial<MethodMap> = {};
1415
+ const isValidMethod = (name: string) =>
1416
+ !name.startsWith("_") &&
1417
+ name !== "constructor" &&
1418
+ !["Middleware", "Pre", "Post", "Raw"].some((s) => name.includes(s));
1419
+
1420
+ for (const [apiName, api] of Object.entries(apiInstances)) {
1421
+ Object.getOwnPropertyNames(Object.getPrototypeOf(api))
1422
+ .filter(
1423
+ (name) =>
1424
+ isValidMethod(name) &&
1425
+ typeof api[name as keyof typeof api] === "function"
1426
+ )
1427
+ .forEach((rawMethodName) => {
1428
+ const simplifiedName = simplifyName(
1429
+ rawMethodName
1430
+ ) as keyof QuickbaseClient;
1431
+ const method = api[rawMethodName as keyof typeof api];
1432
+ const boundMethod = method.bind(api) as unknown;
1433
+ if (typeof boundMethod === "function" && boundMethod.length <= 2) {
1434
+ methodMap[simplifiedName] = {
1435
+ api,
1436
+ method: boundMethod as ApiMethod<typeof simplifiedName>,
1437
+ paramMap: getParamNames(method),
1438
+ };
1439
+ if (debug)
1440
+ console.log(`Mapped ${rawMethodName} to ${simplifiedName}`);
1441
+ }
1442
+ });
1443
+ }
1444
+ return methodMap as MethodMap;
1445
+ }
1446
+
1447
+ const fetchTempToken = async (dbid: string): Promise<string> => {
1448
+ const effectiveFetch = fetchApi || defaultFetch;
1449
+ if (!effectiveFetch) {
1450
+ throw new Error(
1451
+ "No fetch implementation available for fetching temp token"
1452
+ );
1453
+ }
1454
+
1455
+ const response = await effectiveFetch(
1456
+ `https://api.quickbase.com/v1/auth/temporary/${dbid}`,
1457
+ {
1458
+ method: "GET",
1459
+ headers: { ...headers },
1460
+ credentials: "include",
1461
+ }
1462
+ );
1463
+
1464
+ if (!response.ok) {
1465
+ const errorBody = await response.text();
1466
+ throw new Error(`API Error: ${errorBody} (Status: ${response.status})`);
1467
+ }
1468
+
1469
+ const tokenResult = await response.json();
1470
+ const token = tokenResult.temporaryAuthorization;
1471
+ if (!token) {
1472
+ throw new Error("No temporary token returned from API");
1473
+ }
1474
+ tokenCache.set(dbid, token);
1475
+ if (debug) {
1476
+ console.log(
1477
+ `Fetched and cached new token for dbid: ${dbid}`,
1478
+ token,
1479
+ `Expires at: ${new Date(
1480
+ Date.now() + (4 * 60 + 50) * 1000
1481
+ ).toISOString()}`
1482
+ );
1483
+ }
1484
+ return token;
1485
+ };
1486
+
1487
+ const invokeMethod = async <K extends keyof QuickbaseClient>(
1488
+ methodName: K,
1489
+ params: Parameters<QuickbaseClient[K]>[0] & Partial<TempTokenParams>,
1490
+ retryCount: number = 0
1491
+ ): Promise<ReturnType<QuickbaseClient[K]>> => {
1492
+ const methodInfo = methodMap[methodName];
1493
+ if (!methodInfo) {
1494
+ console.error(`Method ${methodName} not found in methodMap`, methodMap);
1495
+ throw new Error(`Method ${methodName} not found`);
1496
+ }
1497
+
1498
+ let token =
1499
+ initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
1500
+ let initOverrides: RequestInit = {};
1501
+
1502
+ // Early return for getTempTokenDBID if cached token exists
1503
+ if (methodName === "getTempTokenDBID" && useTempTokens) {
1504
+ const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
1505
+ const cachedToken = tokenCache.get(dbid);
1506
+ if (cachedToken) {
1507
+ return { temporaryAuthorization: cachedToken } as ReturnType<
1508
+ QuickbaseClient[K]
1509
+ >;
1510
+ }
1511
+ }
1512
+
1513
+ if (useTempTokens && !token) {
1514
+ const dbid = extractDbid(
1515
+ params,
1516
+ `No dbid found in params for ${methodName} to fetch temp token`
1517
+ );
1518
+ if (debug)
1519
+ console.log(`Cache state before fetch for ${dbid}:`, tokenCache.dump());
1520
+ const cachedToken = tokenCache.get(dbid);
1521
+ if (cachedToken) {
1522
+ token = cachedToken;
1523
+ if (debug) console.log(`Reusing cached token for dbid: ${dbid}`, token);
1524
+ } else {
1525
+ if (typeof window === "undefined" && !fetchApi) {
1526
+ throw new Error(
1527
+ "Temporary tokens require a browser environment or a custom fetchApi with browser-like session support"
1528
+ );
1529
+ }
1530
+ token = await fetchTempToken(dbid);
1531
+ // Return immediately for getTempTokenDBID after fetching
1532
+ if (methodName === "getTempTokenDBID") {
1533
+ return { temporaryAuthorization: token } as ReturnType<
1534
+ QuickbaseClient[K]
1535
+ >;
1536
+ }
1537
+ }
1538
+ initOverrides.headers = {
1539
+ ...headers,
1540
+ Authorization: `QB-TEMP-TOKEN ${token}`,
1541
+ };
1542
+ }
1543
+
1544
+ if (debug) console.log(`Invoking ${methodName} with params:`, params);
1545
+ if (debug)
1546
+ console.log(`Calling method with args:`, [params, initOverrides]);
1547
+ const args: [any, RequestInit | undefined] =
1548
+ methodInfo.paramMap.length === 1 &&
1549
+ methodInfo.paramMap[0] === "requestParameters"
1550
+ ? [params, initOverrides]
1551
+ : [params, initOverrides];
1552
+
1553
+ try {
1554
+ const response = await methodInfo.method(...args);
1555
+ if (debug) console.log(`Response from ${methodName}:`, response);
1556
+ return response;
1557
+ } catch (error) {
1558
+ if (
1559
+ error instanceof ResponseError &&
1560
+ error.response.status === 401 &&
1561
+ retryCount < 1
1562
+ ) {
1563
+ if (debug)
1564
+ console.log(
1565
+ `Authorization error for ${methodName}, refreshing token:`,
1566
+ error.message
1567
+ );
1568
+ const dbid = extractDbid(
1569
+ params,
1570
+ `No dbid to refresh token after authorization error`
1571
+ );
1572
+ token = await fetchTempToken(dbid);
1573
+ initOverrides.headers = {
1574
+ ...headers,
1575
+ Authorization: `QB-TEMP-TOKEN ${token}`,
1576
+ };
1577
+ if (debug) console.log(`Retrying ${methodName} with new token`);
1578
+ return invokeMethod(methodName, params, retryCount + 1); // Retry once
1579
+ }
1580
+ if (error instanceof ResponseError) {
1581
+ let errorMessage = error.message;
1582
+ try {
1583
+ const errorBody = await error.response.json();
1584
+ console.log(`Error response body for ${methodName}:`, errorBody);
1585
+ errorMessage = errorBody.message || errorMessage;
1586
+ } catch (e) {
1587
+ console.log(
1588
+ `Failed to parse error response for ${methodName}:`,
1589
+ error.message
1590
+ );
1591
+ }
1592
+ throw new Error(
1593
+ `API Error: ${errorMessage} (Status: ${error.response.status})`
1594
+ );
1595
+ }
1596
+ throw error;
1597
+ }
1598
+ };
1599
+
1600
+ return new Proxy<QuickbaseClient>({} as QuickbaseClient, {
1601
+ get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
1602
+ if (prop in methodMap) {
1603
+ const methodName = prop as keyof QuickbaseClient;
1604
+ return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) => {
1605
+ if (debug) console.log(`Proxy called ${methodName} with:`, params);
1606
+ return invokeMethod(methodName, params);
1607
+ };
1608
+ }
1609
+ console.warn(`Method ${prop} not found in methodMap`);
1610
+ return undefined;
1611
+ },
1612
+ });
1613
+ }
1614
+ - name: specs
1615
+ type: directory
1616
+ children:
1617
+ - name: QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json
1618
+ type: file
1619
+ - name: tokenCache.ts
1620
+ type: file
1621
+ contents: |
1622
+ interface CachedToken {
1623
+ token: string;
1624
+ expiresAt: number; // Timestamp in milliseconds
1625
+ }
1626
+
1627
+ export class TokenCache {
1628
+ private cache: Map<string, CachedToken>;
1629
+ private readonly lifespan: number; // Token lifespan in milliseconds
1630
+
1631
+ constructor(lifespan: number = 4 * 60 * 1000 + 50 * 1000) {
1632
+ // Default 4:50
1633
+ this.cache = new Map<string, CachedToken>();
1634
+ this.lifespan = lifespan;
1635
+ }
1636
+
1637
+ get(dbid: string): string | undefined {
1638
+ const entry = this.cache.get(dbid);
1639
+ const now = Date.now();
1640
+ if (entry && entry.expiresAt > now) {
1641
+ return entry.token;
1642
+ }
1643
+ return undefined; // Expired or not found
1644
+ }
1645
+
1646
+ // New method to get full entry
1647
+ getEntry(dbid: string): CachedToken | undefined {
1648
+ const entry = this.cache.get(dbid);
1649
+ const now = Date.now();
1650
+ if (entry && entry.expiresAt > now) {
1651
+ return entry;
1652
+ }
1653
+ return undefined; // Expired or not found
1654
+ }
1655
+
1656
+ set(dbid: string, token: string): void {
1657
+ const now = Date.now();
1658
+ this.cache.set(dbid, {
1659
+ token,
1660
+ expiresAt: now + this.lifespan,
1661
+ });
1662
+ }
1663
+
1664
+ dump(): [string, CachedToken][] {
1665
+ return Array.from(this.cache.entries());
1666
+ }
1667
+
1668
+ // New method to clear the cache
1669
+ clear(): void {
1670
+ this.cache.clear();
1671
+ }
1672
+ }
1673
+ - name: utils.ts
1674
+ type: file
1675
+ - name: test-artifacts
1676
+ type: directory
1677
+ children:
1678
+ - name: .last-run.json
1679
+ type: file
1680
+ - name: test-results.json
1681
+ type: file
1682
+ - name: test-results.json
1683
+ type: file
1684
+ - name: tests
1685
+ type: directory
1686
+ children:
1687
+ - name: playwright
1688
+ type: directory
1689
+ children:
1690
+ - name: qb
1691
+ type: directory
1692
+ children:
1693
+ - name: auth
1694
+ type: directory
1695
+ children:
1696
+ - name: cacheSwitching.test.ts
1697
+ type: file
1698
+ - name: fetchTempToken401.test.ts
1699
+ type: file
1700
+ - name: getAppWithUserToken.test.ts
1701
+ type: file
1702
+ - name: getTempToken.test.ts
1703
+ type: file
1704
+ - name: tokenPrefetch.test.ts
1705
+ type: file
1706
+ - name: tokenRenewal.test.ts
1707
+ type: file
1708
+ - name: setup.ts
1709
+ type: file
1710
+ - name: vitest
1711
+ type: directory
1712
+ children:
1713
+ - name: qb
1714
+ type: directory
1715
+ children:
1716
+ - name: apps
1717
+ type: directory
1718
+ children:
1719
+ - name: getApp.test.ts
1720
+ type: file
1721
+ - name: fields
1722
+ type: directory
1723
+ children:
1724
+ - name: getFields.test.ts
1725
+ type: file
1726
+ - name: index.ts
1727
+ type: file
1728
+ - name: tables
1729
+ type: directory
1730
+ children:
1731
+ - name: getAppTables.test.ts
1732
+ type: file
1733
+ - name: unit
1734
+ type: directory
1735
+ children:
1736
+ - name: apps
1737
+ type: directory
1738
+ children:
1739
+ - name: getApp.test.ts
1740
+ type: file
1741
+ - name: auth
1742
+ type: directory
1743
+ children:
1744
+ - name: fetchTempToken401.test.ts
1745
+ type: file
1746
+ - name: getTempToken.test.ts
1747
+ type: file
1748
+ - name: retryOn401.test.ts
1749
+ type: file
1750
+ - name: two401s.test.ts
1751
+ type: file
1752
+ - name: fields
1753
+ type: directory
1754
+ children:
1755
+ - name: getFields.test.ts
1756
+ type: file
1757
+ - name: index.ts
1758
+ type: file
1759
+ - name: tables
1760
+ type: directory
1761
+ children:
1762
+ - name: getAppTables.test.ts
1763
+ type: file
1764
+ - name: text.txt
1765
+ type: file
1766
+ - name: tsconfig.json
1767
+ type: file
1768
+ contents: |-
1769
+ {
1770
+ "compilerOptions": {
1771
+ "target": "ESNext",
1772
+ "module": "ESNext", // Changed from NodeNext to match bundler
1773
+ "moduleResolution": "bundler",
1774
+ "strict": true,
1775
+ "lib": ["ESNext", "DOM"],
1776
+ "types": ["node", "playwright/test"],
1777
+ "esModuleInterop": true,
1778
+ "resolveJsonModule": true,
1779
+ "allowImportingTsExtensions": true,
1780
+ "noEmit": true,
1781
+ "outDir": "dist",
1782
+ "rootDir": ".", // Includes src/ and tests/
1783
+ "declaration": true,
1784
+ "sourceMap": true,
1785
+ "baseUrl": ".", // Added to set the root for path resolution
1786
+ "paths": {
1787
+ "@/*": ["./*"] // Maps '@/*' to root directory
1788
+ }
1789
+ },
1790
+ "include": ["src/**/*", "tests/**/*"],
1791
+ "exclude": ["node_modules", "dist", "src/generated-old"],
1792
+ "ts-node": {
1793
+ "esm": true,
1794
+ "transpileOnly": true
1795
+ }
1796
+ }
1797
+ - name: vitest.config.ts
1798
+ type: file
1799
+ contents: |
1800
+ import { defineConfig } from "vitest/config";
1801
+ import path from "path";
1802
+
1803
+ export default defineConfig({
1804
+ test: {
1805
+ environment: "jsdom",
1806
+ include: ["tests/vitest/**/**/*.test.ts"],
1807
+ setupFiles: ["tests/setup.ts"],
1808
+ pool: "forks",
1809
+ poolOptions: {
1810
+ forks: {
1811
+ singleFork: true, // Run tests sequentially
1812
+ },
1813
+ },
1814
+ },
1815
+ resolve: {
1816
+ alias: {
1817
+ "@": path.resolve(__dirname, "./"),
1818
+ },
1819
+ },
1820
+ });