quickbase-js 1.1.0-beta.2 → 1.1.0-beta.3

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/log-gen.yaml CHANGED
@@ -140,6 +140,8 @@ tree:
140
140
  type: file
141
141
  - name: RecordsApi.ts
142
142
  type: file
143
+ - name: RelationshipsApi.ts
144
+ type: file
143
145
  - name: ReportsApi.ts
144
146
  type: file
145
147
  - name: SolutionsApi.ts
@@ -181,6 +183,8 @@ tree:
181
183
  type: file
182
184
  - name: CopyAppRequest.ts
183
185
  type: file
186
+ - name: CopyAppRequestProperties.ts
187
+ type: file
184
188
  - name: CreateApp200Response.ts
185
189
  type: file
186
190
  - name: CreateAppRequest.ts
@@ -223,6 +227,8 @@ tree:
223
227
  type: file
224
228
  - name: GetRelationships200Response.ts
225
229
  type: file
230
+ - name: GetRelationships200ResponseMetadata.ts
231
+ type: file
226
232
  - name: GetReport200Response.ts
227
233
  type: file
228
234
  - name: GetTempTokenDBID200Response.ts
@@ -253,6 +259,12 @@ tree:
253
259
  type: file
254
260
  - name: Record.ts
255
261
  type: file
262
+ - name: RecordValue.ts
263
+ type: file
264
+ - name: Relationship.ts
265
+ type: file
266
+ - name: RelationshipField.ts
267
+ type: file
256
268
  - name: RemoveMembersFromGroup200Response.ts
257
269
  type: file
258
270
  - name: RemoveSubgroupsFromGroup200Response.ts
@@ -263,6 +275,20 @@ tree:
263
275
  type: file
264
276
  - name: RunFormulaRequest.ts
265
277
  type: file
278
+ - name: RunQueryRequest.ts
279
+ type: file
280
+ - name: RunQueryRequestGroupByInner.ts
281
+ type: file
282
+ - name: RunQueryRequestOptions.ts
283
+ type: file
284
+ - name: RunQueryRequestSortByInner.ts
285
+ type: file
286
+ - name: RunQueryResponse.ts
287
+ type: file
288
+ - name: RunQueryResponseFieldsInner.ts
289
+ type: file
290
+ - name: RunQueryResponseMetadata.ts
291
+ type: file
266
292
  - name: Table.ts
267
293
  type: file
268
294
  - name: TransferUserToken200Response.ts
@@ -279,6 +305,8 @@ tree:
279
305
  type: file
280
306
  - name: Upsert200Response.ts
281
307
  type: file
308
+ - name: Upsert200ResponseDataInnerValue.ts
309
+ type: file
282
310
  - name: Upsert200ResponseMetadata.ts
283
311
  type: file
284
312
  - name: Upsert207Response.ts
@@ -296,6 +324,67 @@ tree:
296
324
  children:
297
325
  - name: QuickbaseClient.ts
298
326
  type: file
327
+ contents: |
328
+ // Generated on 2025-03-11T21:24:23.349Z
329
+ import { App, CopyApp200Response, CopyAppRequest, CreateApp200Response, CreateAppRequest, DeleteRecords200Response, DeleteRecordsRequest, DeleteTableResponse, Field, GetRelationships200Response, GetTempTokenDBID200Response, ReportRunResponse, RunQueryRequest, RunQueryResponse, Table, UpdateTableRequest, Upsert200Response, Upsert207Response, UpsertRequest } from "../generated/models";
330
+
331
+ export interface QuickbaseClient {
332
+ createApp: (params: { body: CreateAppRequest }) => Promise<CreateApp200Response>;
333
+ getApp: (params: { appId: string }) => Promise<App>;
334
+ getAppEvents: (params: { appId: string }) => Promise<any[]>;
335
+ copyApp: (params: { appId: string; body: CopyAppRequest }) => Promise<CopyApp200Response>;
336
+ getAppTables: (params: { appId: string }) => Promise<Table[]>;
337
+ createTable: (params: { appId: string; body: any }) => Promise<Table>;
338
+ getTable: (params: { tableId: string; appId: string }) => Promise<Table>;
339
+ updateTable: (params: { tableId: string; appId: string; body: UpdateTableRequest }) => Promise<Table>;
340
+ deleteTable: (params: { tableId: string; appId: string }) => Promise<DeleteTableResponse>;
341
+ getRelationships: (params: { tableId: string; skip?: number }) => Promise<GetRelationships200Response>;
342
+ createRelationship: (params: { tableId: string; generated?: any }) => Promise<{ [key: string]: any }>;
343
+ updateRelationship: (params: { tableId: string; relationshipId: any; generated?: any }) => Promise<{ [key: string]: any }>;
344
+ deleteRelationship: (params: { tableId: string; relationshipId: any }) => Promise<{ [key: string]: any }>;
345
+ getTableReports: (params: { tableId: string }) => Promise<any[]>;
346
+ getReport: (params: { tableId: string; reportId: string }) => Promise<{ [key: string]: any }>;
347
+ runReport: (params: { reportId: string; generated: any }) => Promise<ReportRunResponse[]>;
348
+ getFields: (params: { tableId: string; includeFieldPerms?: boolean }) => Promise<Field[]>;
349
+ getField: (params: { tableId: string; includeFieldPerms?: boolean; fieldId: number }) => Promise<{ [key: string]: any }>;
350
+ updateField: (params: { tableId: string; fieldId: number; generated?: any }) => Promise<{ [key: string]: any }>;
351
+ getFieldsUsage: (params: { tableId: string; skip?: number; top?: number }) => Promise<any[]>;
352
+ getFieldUsage: (params: { tableId: string; fieldId: number }) => Promise<any[]>;
353
+ runFormula: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
354
+ upsert: (params: { body: UpsertRequest }) => Promise<Upsert200Response | Upsert207Response>;
355
+ deleteRecords: (params: { body: DeleteRecordsRequest }) => Promise<DeleteRecords200Response>;
356
+ runQuery: (params: { body: RunQueryRequest }) => Promise<RunQueryResponse>;
357
+ getTempTokenDBID: (params: { dbid: string }) => Promise<GetTempTokenDBID200Response>;
358
+ exchangeSsoToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
359
+ cloneUserToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
360
+ transferUserToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
361
+ deactivateUserToken: (params: { }) => Promise<{ [key: string]: any }>;
362
+ deleteUserToken: (params: { }) => Promise<{ [key: string]: any }>;
363
+ downloadFile: (params: { tableId: string; recordId: number; fieldId: number; versionNumber: number }) => Promise<any>;
364
+ deleteFile: (params: { tableId: string; recordId: number; fieldId: number; versionNumber: number }) => Promise<{ [key: string]: any }>;
365
+ getUsers: (params: { accountId?: any; generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
366
+ denyUsers: (params: { accountId?: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
367
+ denyUsersAndGroups: (params: { accountId?: any; shouldDeleteFromGroups: boolean; generated?: string[] }) => Promise<{ [key: string]: any }>;
368
+ undenyUsers: (params: { accountId?: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
369
+ addMembersToGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
370
+ removeMembersFromGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
371
+ addManagersToGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
372
+ removeManagersFromGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
373
+ addSubgroupsToGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
374
+ removeSubgroupsFromGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
375
+ audit: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
376
+ platformAnalyticReads: (params: { day?: string }) => Promise<{ [key: string]: any }>;
377
+ platformAnalyticEventSummaries: (params: { accountId?: any; generated?: any }) => Promise<any>;
378
+ exportSolution: (params: { solutionId: string; qBLVersion?: string }) => Promise<any>;
379
+ updateSolution: (params: { solutionId: string; generated?: any; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
380
+ createSolution: (params: { generated?: any; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
381
+ exportSolutionToRecord: (params: { solutionId: string; tableId: string; fieldId: number; xQBLErrorsAsSuccess?: boolean; qBLVersion?: string }) => Promise<any>;
382
+ createSolutionFromRecord: (params: { tableId: string; fieldId: number; recordId: number; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
383
+ updateSolutionToRecord: (params: { solutionId: string; tableId: string; fieldId: number; recordId: number; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
384
+ changesetSolution: (params: { solutionId: string; generated?: any; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
385
+ changesetSolutionFromRecord: (params: { solutionId: string; tableId: string; fieldId: number; recordId: number; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
386
+ generateDocument: (params: { templateId: any; tableId: string; recordId?: any; filename: string; accept?: string; format?: string; margin?: string; unit?: string; pageSize?: string; orientation?: string; realm?: string }) => Promise<{ [key: string]: any }>;
387
+ }
299
388
  - name: index.ts
300
389
  type: file
301
390
  - name: log-gen.ts
@@ -307,6 +396,324 @@ tree:
307
396
  type: file
308
397
  - name: quickbaseClient.ts
309
398
  type: file
399
+ contents: |
400
+ // src/quickbaseClient.ts
401
+ import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient.ts";
402
+ import {
403
+ Configuration,
404
+ HTTPHeaders,
405
+ ResponseError,
406
+ } from "./generated/runtime.ts";
407
+ import * as apis from "./generated/apis/index.ts";
408
+ import { TokenCache } from "./tokenCache.ts";
409
+
410
+ // Re-export all model types from generated/models
411
+ export * from "./generated/models";
412
+
413
+ const simplifyName = (name: string): string =>
414
+ name
415
+ .replace(/ById$/, "")
416
+ .replace(/Api$/, "")
417
+ .replace(/^(\w)/, (_, c) => c.toLowerCase());
418
+
419
+ export interface QuickbaseClient extends IQuickbaseClient {}
420
+
421
+ export interface QuickbaseConfig {
422
+ realm: string;
423
+ userToken?: string;
424
+ tempToken?: string;
425
+ useTempTokens?: boolean;
426
+ debug?: boolean;
427
+ fetchApi?: typeof fetch;
428
+ }
429
+
430
+ export interface TempTokenParams {
431
+ appId?: string;
432
+ tableId?: string;
433
+ dbid?: string;
434
+ }
435
+
436
+ type ApiMethod<K extends keyof QuickbaseClient> = (
437
+ requestParameters: Parameters<QuickbaseClient[K]>[0],
438
+ initOverrides?: RequestInit
439
+ ) => Promise<ReturnType<QuickbaseClient[K]>>;
440
+
441
+ interface MethodInfo<K extends keyof QuickbaseClient> {
442
+ api: any;
443
+ method: ApiMethod<K>;
444
+ paramMap: string[];
445
+ }
446
+
447
+ type MethodMap = { [K in keyof QuickbaseClient]: MethodInfo<K> };
448
+
449
+ const getParamNames = (fn: (...args: any[]) => any): string[] =>
450
+ fn
451
+ .toString()
452
+ .slice(fn.toString().indexOf("(") + 1, fn.toString().indexOf(")"))
453
+ .split(",")
454
+ .map((p) => p.trim().split("=")[0].trim())
455
+ .filter((p) => p && !p.match(/^\{/) && p !== "options");
456
+
457
+ const extractDbid = (
458
+ params: Partial<TempTokenParams>,
459
+ errorMessage: string
460
+ ): string => {
461
+ const dbid = params.dbid || params.tableId || params.appId;
462
+ if (!dbid) {
463
+ throw new Error(errorMessage);
464
+ }
465
+ return dbid;
466
+ };
467
+
468
+ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
469
+ const {
470
+ realm,
471
+ userToken,
472
+ tempToken: initialTempToken,
473
+ useTempTokens,
474
+ fetchApi,
475
+ debug,
476
+ } = config;
477
+ const baseUrl = `https://api.quickbase.com/v1`;
478
+
479
+ const tokenCache = new TokenCache();
480
+
481
+ const headers: HTTPHeaders = {
482
+ "QB-Realm-Hostname": `${realm}.quickbase.com`,
483
+ "Content-Type": "application/json",
484
+ };
485
+
486
+ if (initialTempToken) {
487
+ headers["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
488
+ } else if (userToken && !useTempTokens) {
489
+ headers["Authorization"] = `QB-USER-TOKEN ${userToken}`;
490
+ }
491
+
492
+ const defaultFetch: typeof fetch | undefined =
493
+ typeof globalThis.window !== "undefined"
494
+ ? globalThis.window.fetch.bind(globalThis.window)
495
+ : undefined;
496
+ const configuration = new Configuration({
497
+ basePath: baseUrl,
498
+ headers: { ...headers },
499
+ fetchApi: fetchApi || defaultFetch,
500
+ credentials: useTempTokens ? "include" : "omit",
501
+ });
502
+
503
+ if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
504
+ throw new Error(
505
+ "fetchApi must be provided in non-browser environments (e.g., Node.js)"
506
+ );
507
+ }
508
+
509
+ const apiInstances = Object.fromEntries(
510
+ Object.entries(apis)
511
+ .filter(([name]) => name.endsWith("Api"))
512
+ .map(([name, ApiClass]) => [
513
+ name.replace("Api", "").toLowerCase(),
514
+ new ApiClass(configuration),
515
+ ])
516
+ );
517
+
518
+ const methodMap = buildMethodMap();
519
+
520
+ function buildMethodMap(): MethodMap {
521
+ const methodMap: Partial<MethodMap> = {};
522
+ const isValidMethod = (name: string) =>
523
+ !name.startsWith("_") &&
524
+ name !== "constructor" &&
525
+ !["Middleware", "Pre", "Post", "Raw"].some((s) => name.includes(s));
526
+
527
+ for (const [apiName, api] of Object.entries(apiInstances)) {
528
+ Object.getOwnPropertyNames(Object.getPrototypeOf(api))
529
+ .filter(
530
+ (name) =>
531
+ isValidMethod(name) &&
532
+ typeof api[name as keyof typeof api] === "function"
533
+ )
534
+ .forEach((rawMethodName) => {
535
+ const simplifiedName = simplifyName(
536
+ rawMethodName
537
+ ) as keyof QuickbaseClient;
538
+ const method = api[rawMethodName as keyof typeof api];
539
+ const boundMethod = method.bind(api as any) as unknown;
540
+ if (typeof boundMethod === "function" && boundMethod.length <= 2) {
541
+ methodMap[simplifiedName] = {
542
+ api,
543
+ method: boundMethod as ApiMethod<typeof simplifiedName>,
544
+ paramMap: getParamNames(method),
545
+ };
546
+ }
547
+ });
548
+ }
549
+ return methodMap as MethodMap;
550
+ }
551
+
552
+ const fetchTempToken = async (dbid: string): Promise<string> => {
553
+ const effectiveFetch = fetchApi || defaultFetch;
554
+ if (!effectiveFetch) {
555
+ throw new Error(
556
+ "No fetch implementation available for fetching temp token"
557
+ );
558
+ }
559
+
560
+ const response = await effectiveFetch(
561
+ `https://api.quickbase.com/v1/auth/temporary/${dbid}`,
562
+ {
563
+ method: "GET",
564
+ headers: { ...headers },
565
+ credentials: "include",
566
+ }
567
+ );
568
+
569
+ if (!response.ok) {
570
+ const errorBody: { message?: string } = await response.json();
571
+ throw new Error(
572
+ `API Error: ${errorBody.message || "Unknown error"} (Status: ${
573
+ response.status
574
+ })`
575
+ );
576
+ }
577
+
578
+ const tokenResult = await response.json();
579
+ const token = tokenResult.temporaryAuthorization;
580
+ if (!token) {
581
+ throw new Error("No temporary token returned from API");
582
+ }
583
+ tokenCache.set(dbid, token);
584
+ if (debug) {
585
+ console.log(
586
+ `Fetched and cached new token for dbid: ${dbid}`,
587
+ token,
588
+ `Expires at: ${new Date(
589
+ Date.now() + (4 * 60 + 50) * 1000
590
+ ).toISOString()}`
591
+ );
592
+ }
593
+ return token;
594
+ };
595
+
596
+ const invokeMethod = async <K extends keyof QuickbaseClient>(
597
+ methodName: K,
598
+ params: Parameters<QuickbaseClient[K]>[0] & Partial<TempTokenParams>,
599
+ retryCount: number = 0
600
+ ): Promise<ReturnType<QuickbaseClient[K]>> => {
601
+ const methodInfo = methodMap[methodName];
602
+ if (!methodInfo) {
603
+ throw new Error(`Method ${methodName} not found`);
604
+ }
605
+
606
+ const args: [any, RequestInit | undefined] =
607
+ methodInfo.paramMap.length === 1 &&
608
+ methodInfo.paramMap[0] === "requestParameters"
609
+ ? [{ requestParameters: params }, undefined]
610
+ : [params, undefined];
611
+
612
+ let token =
613
+ initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
614
+
615
+ if (methodName === "getTempTokenDBID" && useTempTokens) {
616
+ const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
617
+ const cachedToken = tokenCache.get(dbid);
618
+ if (cachedToken) {
619
+ return { temporaryAuthorization: cachedToken } as ReturnType<
620
+ QuickbaseClient[K]
621
+ >;
622
+ }
623
+ }
624
+
625
+ if (useTempTokens && !token) {
626
+ const dbid = extractDbid(
627
+ params,
628
+ `No dbid found in params for ${methodName} to fetch temp token`
629
+ );
630
+ const cachedToken = tokenCache.get(dbid);
631
+ if (cachedToken) {
632
+ token = cachedToken;
633
+ } else {
634
+ if (typeof globalThis.window === "undefined" && !fetchApi) {
635
+ throw new Error(
636
+ "Temporary tokens require a browser environment or a custom fetchApi with browser-like session support"
637
+ );
638
+ }
639
+ token = await fetchTempToken(dbid);
640
+ if (methodName === "getTempTokenDBID") {
641
+ return { temporaryAuthorization: token } as ReturnType<
642
+ QuickbaseClient[K]
643
+ >;
644
+ }
645
+ }
646
+ args[1] = {
647
+ headers: {
648
+ ...headers,
649
+ Authorization: `QB-TEMP-TOKEN ${token}`,
650
+ },
651
+ };
652
+ }
653
+
654
+ try {
655
+ const response = await methodInfo.method(...args);
656
+ return response;
657
+ } catch (error) {
658
+ if (
659
+ error instanceof ResponseError &&
660
+ error.response.status === 401 &&
661
+ retryCount < 1
662
+ ) {
663
+ if (debug) {
664
+ console.log(
665
+ `Authorization error for ${methodName}, refreshing token:`,
666
+ error.message
667
+ );
668
+ }
669
+ const dbid = extractDbid(
670
+ params,
671
+ `No dbid to refresh token after authorization error`
672
+ );
673
+ token = await fetchTempToken(dbid);
674
+ args[1] = {
675
+ headers: {
676
+ ...headers,
677
+ Authorization: `QB-TEMP-TOKEN ${token}`,
678
+ },
679
+ };
680
+ if (debug) {
681
+ console.log(`Retrying ${methodName} with new token`);
682
+ }
683
+ return invokeMethod(methodName, params, retryCount + 1);
684
+ }
685
+ if (error instanceof ResponseError) {
686
+ let errorMessage = error.message;
687
+ try {
688
+ const errorBody: { message?: string } = await error.response.json();
689
+ if (debug) {
690
+ console.log(`Error response body for ${methodName}:`, errorBody);
691
+ }
692
+ errorMessage = errorBody.message || errorMessage;
693
+ } catch (e) {
694
+ // Silent fail on parse error
695
+ }
696
+ throw new Error(
697
+ `API Error: ${errorMessage} (Status: ${error.response.status})`
698
+ );
699
+ }
700
+ throw error;
701
+ }
702
+ };
703
+
704
+ const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, {
705
+ get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
706
+ if (prop in methodMap) {
707
+ const methodName = prop as keyof QuickbaseClient;
708
+ return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
709
+ invokeMethod(methodName, params);
710
+ }
711
+ return undefined;
712
+ },
713
+ });
714
+
715
+ return proxy;
716
+ }
310
717
  - name: tokenCache.ts
311
718
  type: file
312
719
  - name: test-artifacts
@@ -332,1091 +739,18 @@ tree:
332
739
  children:
333
740
  - name: cacheSwitching.test.ts
334
741
  type: file
335
- contents: |
336
- // tests/playwright/qb/auth/cacheSwitching.test.ts
337
- import { test, expect } from "@playwright/test";
338
- import { quickbase } from "../../../../src/quickbaseClient.ts";
339
- import fetch from "node-fetch";
340
- import dotenv from "dotenv";
341
-
342
- dotenv.config();
343
-
344
- const loginToQuickbase = async (
345
- page: import("@playwright/test").Page,
346
- quickbaseUrl: string,
347
- username: string,
348
- password: string
349
- ): Promise<boolean> => {
350
- console.log("Logging in to QuickBase");
351
- let loginSuccess = false;
352
- const maxLoginAttempts = 3;
353
- let loginAttempt = 0;
354
-
355
- while (!loginSuccess && loginAttempt < maxLoginAttempts) {
356
- try {
357
- loginAttempt++;
358
- if (loginAttempt > 1) console.log(`Login attempt ${loginAttempt}`);
359
-
360
- console.log("Navigating to login page");
361
- await page.goto(quickbaseUrl, { timeout: 30000 });
362
- console.log("Waiting for loginid selector");
363
- await page.waitForSelector("input[name='loginid']", { timeout: 30000 });
364
- await page.fill("input[name='loginid']", username);
365
- await page.fill("input[name='password']", password);
366
- console.log("Clicking signin");
367
- await page.click("#signin");
368
- console.log("Waiting for navigation");
369
- await page.waitForURL(`https://*.quickbase.com/**`, {
370
- waitUntil: "networkidle",
371
- timeout: 30000,
372
- });
373
-
374
- const loginError = await page.$(".login-error");
375
- if (loginError) {
376
- throw new Error("Login failed. Please check your credentials.");
377
- }
378
-
379
- console.log("Signed In to QuickBase.");
380
- loginSuccess = true;
381
- } catch (error) {
382
- console.error(
383
- `Login attempt ${loginAttempt} failed: ${(error as Error).message}`
384
- );
385
- if (loginAttempt >= maxLoginAttempts) {
386
- console.error("Max login attempts reached. Exiting.");
387
- await page.context().browser()!.close();
388
- return false;
389
- }
390
- await new Promise((resolve) => setTimeout(resolve, 5000));
391
- }
392
- }
393
- return loginSuccess;
394
- };
395
-
396
- test.describe("QuickbaseClient Integration - Cache Switching with Temp Tokens", () => {
397
- // Removed test.use({ context: ... })
398
-
399
- test("caches and switches tokens for different DBIDs with getApp → getFields → getApp sequence", async ({
400
- page,
401
- }) => {
402
- console.log("Starting cacheSwitching test");
403
- const realm = process.env.QB_REALM;
404
- const appId = process.env.QB_APP_ID;
405
- const tableId = process.env.QB_TABLE_ID;
406
- const username = process.env.QB_USERNAME;
407
- const password = process.env.QB_PASSWORD;
408
-
409
- if (!realm) throw new Error("QB_REALM is not defined in .env");
410
- if (!appId) throw new Error("QB_APP_ID is not defined in .env");
411
- if (!tableId) throw new Error("QB_TABLE_ID is not defined in .env");
412
- if (!username) throw new Error("QB_USERNAME is not defined in .env");
413
- if (!password) throw new Error("QB_PASSWORD is not defined in .env");
414
-
415
- const quickbaseUrl = `https://${realm}.quickbase.com/db/main?a=SignIn`;
416
- const loginSuccess = await loginToQuickbase(
417
- page,
418
- quickbaseUrl,
419
- username,
420
- password
421
- );
422
- if (!loginSuccess) {
423
- throw new Error("Failed to log in to QuickBase after max attempts.");
424
- }
425
-
426
- await page.goto(`https://${realm}.quickbase.com/db/${appId}`, {
427
- waitUntil: "networkidle",
428
- timeout: 30000,
429
- });
430
- console.log("Post-login URL after app navigation:", page.url());
431
-
432
- const browserFetch = async (url: string, init?: RequestInit) => {
433
- const response = await page.evaluate(
434
- async ([fetchUrl, fetchInit]) => {
435
- const res = await fetch(fetchUrl, {
436
- ...fetchInit,
437
- credentials: "include",
438
- });
439
- const body = await res.text();
440
- return {
441
- ok: res.ok,
442
- status: res.status,
443
- statusText: res.statusText,
444
- headers: Object.fromEntries(res.headers.entries()),
445
- body,
446
- };
447
- },
448
- [url, init] as [string, RequestInit]
449
- );
450
-
451
- console.log(`Raw response from ${url}:`, {
452
- status: response.status,
453
- statusText: response.statusText,
454
- body: response.body,
455
- });
456
-
457
- const fetchResponse = new Response(response.body || null, {
458
- status: response.status,
459
- statusText: response.statusText,
460
- headers: response.headers,
461
- });
462
-
463
- fetchResponse.json = async () => {
464
- if (!response.body) throw new Error("Empty response body from API");
465
- try {
466
- return JSON.parse(response.body);
467
- } catch (e) {
468
- throw new SyntaxError(`Invalid JSON response: ${response.body}`);
469
- }
470
- };
471
-
472
- return fetchResponse;
473
- };
474
-
475
- const client = quickbase({
476
- realm,
477
- useTempTokens: true,
478
- debug: true,
479
- fetchApi: async (url, init) => {
480
- if (url.includes("/auth/temporary/")) {
481
- return browserFetch(url, init);
482
- }
483
- console.log(`Fetching ${url} with init:`, init);
484
- const response = await fetch(url, init as RequestInit);
485
- const body = await response.text();
486
-
487
- console.log(`Raw response from ${url}:`, {
488
- status: response.status,
489
- statusText: response.statusText,
490
- body,
491
- });
492
-
493
- const fetchResponse = new Response(body || null, {
494
- status: response.status,
495
- statusText: response.statusText,
496
- headers: Object.fromEntries(response.headers.entries()),
497
- });
498
-
499
- fetchResponse.json = async () => {
500
- if (!body) throw new Error("Empty response body from API");
501
- try {
502
- return JSON.parse(body);
503
- } catch (e) {
504
- throw new SyntaxError(`Invalid JSON response: ${body}`);
505
- }
506
- };
507
-
508
- return fetchResponse;
509
- },
510
- });
511
-
512
- const appResult1 = await client.getApp({ appId });
513
- console.log("First getApp response:", appResult1);
514
- expect(appResult1.id).toBe(appId);
515
-
516
- const fieldsResult = await client.getFields({ tableId });
517
- console.log("getFields response:", fieldsResult);
518
- expect(fieldsResult.length).toBeGreaterThan(0);
519
-
520
- const appResult2 = await client.getApp({ appId });
521
- console.log("Second getApp response:", appResult2);
522
- expect(appResult2.id).toBe(appId);
523
- });
524
- });
525
742
  - name: fetchTempToken401.test.ts
526
743
  type: file
527
- contents: |
528
- // tests/playwright/qb/auth/tokenInvalidation.test.ts
529
- import { test, expect } from "@playwright/test";
530
- import { quickbase } from "../../../../src/quickbaseClient.ts";
531
- import fetch from "node-fetch";
532
- import dotenv from "dotenv";
533
-
534
- dotenv.config();
535
-
536
- const loginToQuickbase = async (
537
- page: import("@playwright/test").Page,
538
- quickbaseUrl: string,
539
- username: string,
540
- password: string
541
- ): Promise<boolean> => {
542
- console.log("Logging in to QuickBase");
543
- let loginSuccess = false;
544
- const maxLoginAttempts = 3;
545
- let loginAttempt = 0;
546
-
547
- while (!loginSuccess && loginAttempt < maxLoginAttempts) {
548
- try {
549
- loginAttempt++;
550
- if (loginAttempt > 1) console.log(`Login attempt ${loginAttempt}`);
551
-
552
- await page.goto(quickbaseUrl, { timeout: 60000 });
553
- await page.waitForSelector("input[name='loginid']", { timeout: 60000 });
554
- await page.fill("input[name='loginid']", username);
555
- await page.fill("input[name='password']", password);
556
- await page.click("#signin");
557
- await page.waitForURL(`https://*.quickbase.com/**`, {
558
- waitUntil: "networkidle",
559
- timeout: 60000,
560
- });
561
-
562
- const loginError = await page.$(".login-error");
563
- if (loginError) {
564
- throw new Error("Login failed. Please check your credentials.");
565
- }
566
-
567
- console.log("Signed In to QuickBase.");
568
- loginSuccess = true;
569
- } catch (error) {
570
- console.error(
571
- `Login attempt ${loginAttempt} failed: ${(error as Error).message}`
572
- );
573
- if (loginAttempt >= maxLoginAttempts) {
574
- console.error("Max login attempts reached. Exiting.");
575
- await page.context().browser()!.close();
576
- return false;
577
- }
578
- await new Promise((resolve) => setTimeout(resolve, 5000));
579
- }
580
- }
581
- return loginSuccess;
582
- };
583
-
584
- test.describe("QuickbaseClient Integration - Token Invalidation with Cached Token", () => {
585
- test("retries with new token when cached token is unexpectedly invalidated", async ({
586
- page,
587
- }) => {
588
- const realm = process.env.QB_REALM;
589
- const appId = process.env.QB_APP_ID;
590
- const username = process.env.QB_USERNAME;
591
- const password = process.env.QB_PASSWORD;
592
-
593
- if (!realm) throw new Error("QB_REALM is not defined in .env");
594
- if (!appId) throw new Error("QB_APP_ID is not defined in .env");
595
- if (!username) throw new Error("QB_USERNAME is not defined in .env");
596
- if (!password) throw new Error("QB_PASSWORD is not defined in .env");
597
-
598
- // Login to QuickBase
599
- const quickbaseUrl = `https://${realm}.quickbase.com/db/main?a=SignIn`;
600
- const loginSuccess = await loginToQuickbase(
601
- page,
602
- quickbaseUrl,
603
- username,
604
- password
605
- );
606
- if (!loginSuccess) {
607
- throw new Error("Failed to log in to QuickBase after max attempts.");
608
- }
609
-
610
- // Navigate to the app page to ensure session context
611
- await page.goto(`https://${realm}.quickbase.com/db/${appId}`, {
612
- waitUntil: "networkidle",
613
- timeout: 60000,
614
- });
615
- console.log("Post-login URL after app navigation:", page.url());
616
-
617
- // Track tokens and call counts
618
- let callCount = 0;
619
- let getAppCallCount = 0;
620
- let initialToken: string | null = null;
621
- let retryToken: string | null = null;
622
-
623
- const browserFetch = async (url: string, init?: RequestInit) => {
624
- console.log(`browserFetch called for ${url}, callCount: ${callCount}`);
625
- const response = await page.evaluate(
626
- async ([fetchUrl, fetchInit]) => {
627
- const res = await fetch(fetchUrl, {
628
- ...fetchInit,
629
- credentials: "include",
630
- });
631
- const body = await res.text();
632
- return {
633
- ok: res.ok,
634
- status: res.status,
635
- statusText: res.statusText,
636
- headers: Object.fromEntries(res.headers.entries()),
637
- body,
638
- };
639
- },
640
- [url, init] as [string, RequestInit]
641
- );
642
-
643
- console.log(`Raw response from ${url}:`, {
644
- status: response.status,
645
- statusText: response.statusText,
646
- body: response.body,
647
- });
648
-
649
- const fetchResponse = new Response(response.body || null, {
650
- status: response.status,
651
- statusText: response.statusText,
652
- headers: response.headers,
653
- });
654
-
655
- // Capture token from response
656
- if (url.includes("/auth/temporary/")) {
657
- const json = await fetchResponse.clone().json();
658
- const token = json.temporaryAuthorization;
659
- if (!initialToken) {
660
- initialToken = token;
661
- console.log(`Captured initial token: ${initialToken}`);
662
- } else if (!retryToken) {
663
- retryToken = token;
664
- console.log(`Captured retry token: ${retryToken}`);
665
- }
666
- }
667
-
668
- return fetchResponse;
669
- };
670
-
671
- const client = quickbase({
672
- realm,
673
- useTempTokens: true,
674
- debug: true,
675
- fetchApi: async (url, init) => {
676
- callCount++;
677
- console.log(`fetchApi called for ${url}, callCount: ${callCount}`);
678
-
679
- if (url.includes("/auth/temporary/")) {
680
- return browserFetch(url, init); // Use browser fetch for temp tokens
681
- }
682
-
683
- if (url.includes(`/apps/${appId}`)) {
684
- getAppCallCount++;
685
- console.log(`getApp call count: ${getAppCallCount}`);
686
- // Simulate 401 on the first getApp call to trigger retry
687
- if (getAppCallCount === 1) {
688
- console.log("Simulating 401 Unauthorized for first getApp call");
689
- return new Response(JSON.stringify({ message: "Unauthorized" }), {
690
- status: 401,
691
- statusText: "Unauthorized",
692
- headers: { "Content-Type": "application/json" },
693
- });
694
- }
695
- }
696
-
697
- // Use node-fetch for subsequent calls (including retries)
698
- console.log(`Fetching ${url} with init:`, init);
699
- const response = await fetch(url, init as RequestInit);
700
- const body = await response.text();
701
-
702
- console.log(`Raw response from ${url}:`, {
703
- status: response.status,
704
- statusText: response.statusText,
705
- body,
706
- });
707
-
708
- return new Response(body || null, {
709
- status: response.status,
710
- statusText: response.statusText,
711
- headers: Object.fromEntries(response.headers.entries()),
712
- });
713
- },
714
- });
715
-
716
- // Pre-fetch token to simulate cached token
717
- console.log("Pre-fetching initial token...");
718
- const preFetchResult = await client.getTempTokenDBID({ dbid: appId });
719
- console.log("Initial token pre-fetched and cached:", preFetchResult);
720
-
721
- // Call getApp, expecting a 401 followed by a retry with a new token
722
- const appResult = await client.getApp({ appId });
723
- console.log("App response after token invalidation and retry:", appResult);
724
-
725
- // Validate the app response
726
- expect(appResult).toHaveProperty("id", appId);
727
- expect(appResult).toHaveProperty("name");
728
-
729
- // Verify call counts and token behavior
730
- console.log(
731
- `Final callCount: ${callCount}, Final getAppCallCount: ${getAppCallCount}`
732
- );
733
- expect(callCount).toBe(4); // Pre-fetch, 401, token retry, getApp success
734
- expect(getAppCallCount).toBe(2); // Initial 401, retry success
735
-
736
- // Validate pre-fetch and retry tokens
737
- console.log(`Initial token: ${initialToken}`);
738
- console.log(`Retry token: ${retryToken}`);
739
- expect(initialToken).toBeDefined();
740
- expect(initialToken).toBe(preFetchResult.temporaryAuthorization); // Confirm pre-fetch token was used
741
- expect(retryToken).toBeDefined();
742
- expect(initialToken).not.toBe(retryToken); // Confirm retry used a new token
743
- });
744
- });
745
744
  - name: getAppWithUserToken.test.ts
746
745
  type: file
747
- contents: |
748
- // tests/integration/auth/getAppWithUserToken.test.ts
749
- import { test, expect } from "@playwright/test";
750
- import { quickbase } from "../../../../src/quickbaseClient.ts";
751
- import dotenv from "dotenv";
752
-
753
- dotenv.config();
754
-
755
- const loginToQuickbase = async (
756
- page: import("@playwright/test").Page,
757
- quickbaseUrl: string,
758
- username: string,
759
- password: string
760
- ): Promise<boolean> => {
761
- console.log("Logging in to QuickBase");
762
- let loginSuccess = false;
763
- const maxLoginAttempts = 3;
764
- let loginAttempt = 0;
765
-
766
- while (!loginSuccess && loginAttempt < maxLoginAttempts) {
767
- try {
768
- loginAttempt++;
769
- if (loginAttempt > 1) console.log(`Login attempt ${loginAttempt}`);
770
-
771
- await page.goto(quickbaseUrl, { timeout: 60000 });
772
- await page.waitForSelector("input[name='loginid']", { timeout: 60000 });
773
- await page.fill("input[name='loginid']", username);
774
- await page.fill("input[name='password']", password);
775
- await page.click("#signin");
776
- await page.waitForURL(`https://*.quickbase.com/**`, {
777
- waitUntil: "networkidle",
778
- timeout: 60000,
779
- });
780
-
781
- const loginError = await page.$(".login-error");
782
- if (loginError) {
783
- throw new Error("Login failed. Please check your credentials.");
784
- }
785
-
786
- console.log("Signed In to QuickBase.");
787
- loginSuccess = true;
788
- } catch (error) {
789
- console.error(
790
- `Login attempt ${loginAttempt} failed: ${(error as Error).message}`
791
- );
792
- if (loginAttempt >= maxLoginAttempts) {
793
- console.error("Max login attempts reached. Exiting.");
794
- await page.context().browser()!.close();
795
- return false;
796
- }
797
- await new Promise((resolve) => setTimeout(resolve, 5000));
798
- }
799
- }
800
- return loginSuccess;
801
- };
802
-
803
- test.describe("QuickbaseClient Integration - getApp with User Token", () => {
804
- test("uses user token for getApp in browser", async ({ page }) => {
805
- const realm = process.env.QB_REALM;
806
- const appId = process.env.QB_APP_ID;
807
- const username = process.env.QB_USERNAME;
808
- const password = process.env.QB_PASSWORD;
809
- const userToken = process.env.QB_USER_TOKEN;
810
-
811
- if (!realm) throw new Error("QB_REALM is not defined in .env");
812
- if (!appId) throw new Error("QB_APP_ID is not defined in .env");
813
- if (!username) throw new Error("QB_USERNAME is not defined in .env");
814
- if (!password) throw new Error("QB_PASSWORD is not defined in .env");
815
- if (!userToken) throw new Error("QB_USER_TOKEN is not defined in .env");
816
-
817
- // Login to Quickbase (optional, to mimic code page context)
818
- const quickbaseUrl = `https://${realm}.quickbase.com/db/main?a=SignIn`;
819
- const loginSuccess = await loginToQuickbase(
820
- page,
821
- quickbaseUrl,
822
- username,
823
- password
824
- );
825
- if (!loginSuccess) {
826
- throw new Error("Failed to log in to QuickBase after max attempts.");
827
- }
828
-
829
- // Navigate to the app page to ensure session context (optional)
830
- await page.goto(`https://${realm}.quickbase.com/db/${appId}`, {
831
- waitUntil: "networkidle",
832
- timeout: 60000,
833
- });
834
-
835
- const currentUrl = page.url();
836
- console.log("Post-login URL after app navigation:", currentUrl);
837
-
838
- // Create Quickbase client with user token
839
- const client = quickbase({
840
- realm,
841
- userToken, // Use user token instead of temp tokens
842
- debug: true,
843
- fetchApi: async (url, init) => {
844
- console.log(`Fetching ${url} with init:`, init);
845
- const response = await page.evaluate(
846
- async ([fetchUrl, fetchInit]) => {
847
- const res = await fetch(fetchUrl, {
848
- ...fetchInit,
849
- credentials: "omit", // No cookies needed with user token
850
- });
851
- const body = await res.text();
852
- return {
853
- ok: res.ok,
854
- status: res.status,
855
- statusText: res.statusText,
856
- headers: Object.fromEntries(res.headers.entries()),
857
- body,
858
- };
859
- },
860
- [url, init] as [string, RequestInit]
861
- );
862
-
863
- console.log(`Raw response from ${url}:`, {
864
- status: response.status,
865
- statusText: response.statusText,
866
- body: response.body,
867
- });
868
-
869
- const fetchResponse = new Response(response.body || null, {
870
- status: response.status,
871
- statusText: response.statusText,
872
- headers: response.headers,
873
- });
874
-
875
- fetchResponse.json = async () => {
876
- if (!response.body) {
877
- throw new Error("Empty response body from API");
878
- }
879
- try {
880
- return JSON.parse(response.body);
881
- } catch (e) {
882
- throw new SyntaxError(`Invalid JSON response: ${response.body}`);
883
- }
884
- };
885
-
886
- return fetchResponse;
887
- },
888
- });
889
-
890
- // Call getApp directly
891
- const appResult = await client.getApp({ appId });
892
- console.log("App response using user token:", appResult);
893
-
894
- // Validate the app response
895
- expect(appResult).toHaveProperty("id", appId);
896
- expect(appResult).toHaveProperty("name");
897
- });
898
- });
899
746
  - name: getTempToken.test.ts
900
747
  type: file
901
- contents: |
902
- // tests/integration/auth/getTempToken.test.ts
903
- import { test, expect } from "@playwright/test";
904
- import { quickbase } from "../../../../src/quickbaseClient.ts";
905
- import fetch from "node-fetch";
906
- import dotenv from "dotenv";
907
-
908
- dotenv.config();
909
-
910
- const loginToQuickbase = async (
911
- page: import("@playwright/test").Page,
912
- quickbaseUrl: string,
913
- username: string,
914
- password: string
915
- ): Promise<boolean> => {
916
- console.log("Logging in to QuickBase");
917
- let loginSuccess = false;
918
- const maxLoginAttempts = 3;
919
- let loginAttempt = 0;
920
-
921
- while (!loginSuccess && loginAttempt < maxLoginAttempts) {
922
- try {
923
- loginAttempt++;
924
- if (loginAttempt > 1) console.log(`Login attempt ${loginAttempt}`);
925
-
926
- await page.goto(quickbaseUrl, { timeout: 60000 });
927
- await page.waitForSelector("input[name='loginid']", { timeout: 60000 });
928
- await page.fill("input[name='loginid']", username);
929
- await page.fill("input[name='password']", password);
930
- await page.click("#signin");
931
- await page.waitForURL(`https://*.quickbase.com/**`, {
932
- waitUntil: "networkidle",
933
- timeout: 60000,
934
- });
935
-
936
- const loginError = await page.$(".login-error");
937
- if (loginError) {
938
- throw new Error("Login failed. Please check your credentials.");
939
- }
940
-
941
- console.log("Signed In to QuickBase.");
942
- loginSuccess = true;
943
- } catch (error) {
944
- console.error(
945
- `Login attempt ${loginAttempt} failed: ${(error as Error).message}`
946
- );
947
- if (loginAttempt >= maxLoginAttempts) {
948
- console.error("Max login attempts reached. Exiting.");
949
- await page.context().browser()!.close();
950
- return false;
951
- }
952
- await new Promise((resolve) => setTimeout(resolve, 5000));
953
- }
954
- }
955
- return loginSuccess;
956
- };
957
-
958
- test.describe("QuickbaseClient Integration - getApp with Temp Tokens", () => {
959
- test("uses temp tokens automatically for getApp in browser", async ({
960
- page,
961
- }) => {
962
- const realm = process.env.QB_REALM;
963
- const appId = process.env.QB_APP_ID;
964
- const username = process.env.QB_USERNAME;
965
- const password = process.env.QB_PASSWORD;
966
-
967
- if (!realm) throw new Error("QB_REALM is not defined in .env");
968
- if (!appId) throw new Error("QB_APP_ID is not defined in .env");
969
- if (!username) throw new Error("QB_USERNAME is not defined in .env");
970
- if (!password) throw new Error("QB_PASSWORD is not defined in .env");
971
-
972
- // Login to QuickBase
973
- const quickbaseUrl = `https://${realm}.quickbase.com/db/main?a=SignIn`;
974
- const loginSuccess = await loginToQuickbase(
975
- page,
976
- quickbaseUrl,
977
- username,
978
- password
979
- );
980
- if (!loginSuccess) {
981
- throw new Error("Failed to log in to QuickBase after max attempts.");
982
- }
983
-
984
- // Navigate to the app page to ensure session context
985
- await page.goto(`https://${realm}.quickbase.com/db/${appId}`, {
986
- waitUntil: "networkidle",
987
- timeout: 60000,
988
- });
989
- console.log("Post-login URL after app navigation:", page.url());
990
-
991
- // Define browser fetch function
992
- const browserFetch = async (url: string, init?: RequestInit) => {
993
- const response = await page.evaluate(
994
- async ([fetchUrl, fetchInit]) => {
995
- const res = await fetch(fetchUrl, {
996
- ...fetchInit,
997
- credentials: "include",
998
- });
999
- const body = await res.text();
1000
- return {
1001
- ok: res.ok,
1002
- status: res.status,
1003
- statusText: res.statusText,
1004
- headers: Object.fromEntries(res.headers.entries()),
1005
- body,
1006
- };
1007
- },
1008
- [url, init] as [string, RequestInit]
1009
- );
1010
-
1011
- console.log(`Raw response from ${url}:`, {
1012
- status: response.status,
1013
- statusText: response.statusText,
1014
- body: response.body,
1015
- });
1016
-
1017
- const fetchResponse = new Response(response.body || null, {
1018
- status: response.status,
1019
- statusText: response.statusText,
1020
- headers: response.headers,
1021
- });
1022
-
1023
- fetchResponse.json = async () => {
1024
- if (!response.body) throw new Error("Empty response body from API");
1025
- try {
1026
- return JSON.parse(response.body);
1027
- } catch (e) {
1028
- throw new SyntaxError(`Invalid JSON response: ${response.body}`);
1029
- }
1030
- };
1031
-
1032
- return fetchResponse;
1033
- };
1034
-
1035
- // Single client with conditional fetch
1036
- const client = quickbase({
1037
- realm,
1038
- useTempTokens: true,
1039
- debug: true,
1040
- fetchApi: async (url, init) => {
1041
- if (url.includes("/auth/temporary/")) {
1042
- return browserFetch(url, init); // Use browser context for temp token
1043
- }
1044
- console.log(`Fetching ${url} with init:`, init);
1045
- const response = await fetch(url, init as RequestInit); // Node-fetch for API calls
1046
- const body = await response.text();
1047
-
1048
- console.log(`Raw response from ${url}:`, {
1049
- status: response.status,
1050
- statusText: response.statusText,
1051
- body,
1052
- });
1053
-
1054
- const fetchResponse = new Response(body || null, {
1055
- status: response.status,
1056
- statusText: response.statusText,
1057
- headers: Object.fromEntries(response.headers.entries()),
1058
- });
1059
-
1060
- fetchResponse.json = async () => {
1061
- if (!body) throw new Error("Empty response body from API");
1062
- try {
1063
- return JSON.parse(body);
1064
- } catch (e) {
1065
- throw new SyntaxError(`Invalid JSON response: ${body}`);
1066
- }
1067
- };
1068
-
1069
- return fetchResponse;
1070
- },
1071
- });
1072
-
1073
- // Call getApp directly, letting the library handle temp token logic
1074
- const appResult = await client.getApp({ appId });
1075
- console.log("App response using auto-fetched temp token:", appResult);
1076
-
1077
- // Validate the app response
1078
- expect(appResult).toHaveProperty("id", appId);
1079
- expect(appResult).toHaveProperty("name");
1080
- });
1081
- });
1082
748
  - name: tokenPrefetch.test.ts
1083
749
  type: file
1084
- contents: |
1085
- // tests/playwright/qb/auth/tokenPrefetch.test.ts
1086
- import { test, expect } from "@playwright/test";
1087
- import { quickbase } from "../../../../src/quickbaseClient.ts";
1088
- import fetch from "node-fetch";
1089
- import dotenv from "dotenv";
1090
-
1091
- dotenv.config();
1092
-
1093
- const loginToQuickbase = async (
1094
- page: import("@playwright/test").Page,
1095
- quickbaseUrl: string,
1096
- username: string,
1097
- password: string
1098
- ): Promise<boolean> => {
1099
- console.log("Logging in to QuickBase");
1100
- let loginSuccess = false;
1101
- const maxLoginAttempts = 3;
1102
- let loginAttempt = 0;
1103
-
1104
- while (!loginSuccess && loginAttempt < maxLoginAttempts) {
1105
- try {
1106
- loginAttempt++;
1107
- if (loginAttempt > 1) console.log(`Login attempt ${loginAttempt}`);
1108
-
1109
- await page.goto(quickbaseUrl, { timeout: 60000 });
1110
- await page.waitForSelector("input[name='loginid']", { timeout: 60000 });
1111
- await page.fill("input[name='loginid']", username);
1112
- await page.fill("input[name='password']", password);
1113
- await page.click("#signin");
1114
- await page.waitForURL(`https://*.quickbase.com/**`, {
1115
- waitUntil: "networkidle",
1116
- timeout: 60000,
1117
- });
1118
-
1119
- const loginError = await page.$(".login-error");
1120
- if (loginError) {
1121
- throw new Error("Login failed. Please check your credentials.");
1122
- }
1123
-
1124
- console.log("Signed In to QuickBase.");
1125
- loginSuccess = true;
1126
- } catch (error) {
1127
- console.error(
1128
- `Login attempt ${loginAttempt} failed: ${(error as Error).message}`
1129
- );
1130
- if (loginAttempt >= maxLoginAttempts) {
1131
- console.error("Max login attempts reached. Exiting.");
1132
- await page.context().browser()!.close();
1133
- return false;
1134
- }
1135
- await new Promise((resolve) => setTimeout(resolve, 5000));
1136
- }
1137
- }
1138
- return loginSuccess;
1139
- };
1140
-
1141
- test.describe("QuickbaseClient Integration - Token Pre-Fetch and Cache Reuse", () => {
1142
- test("pre-fetches token and reuses it for subsequent API calls", async ({
1143
- page,
1144
- }) => {
1145
- const realm = process.env.QB_REALM;
1146
- const appId = process.env.QB_APP_ID;
1147
- const username = process.env.QB_USERNAME;
1148
- const password = process.env.QB_PASSWORD;
1149
-
1150
- if (!realm) throw new Error("QB_REALM is not defined in .env");
1151
- if (!appId) throw new Error("QB_APP_ID is not defined in .env");
1152
- if (!username) throw new Error("QB_USERNAME is not defined in .env");
1153
- if (!password) throw new Error("QB_PASSWORD is not defined in .env");
1154
-
1155
- // Login to QuickBase
1156
- const quickbaseUrl = `https://${realm}.quickbase.com/db/main?a=SignIn`;
1157
- const loginSuccess = await loginToQuickbase(
1158
- page,
1159
- quickbaseUrl,
1160
- username,
1161
- password
1162
- );
1163
- if (!loginSuccess) {
1164
- throw new Error("Failed to log in to QuickBase after max attempts.");
1165
- }
1166
-
1167
- // Navigate to the app page to ensure session context
1168
- await page.goto(`https://${realm}.quickbase.com/db/${appId}`, {
1169
- waitUntil: "networkidle",
1170
- timeout: 60000,
1171
- });
1172
- console.log("Post-login URL after app navigation:", page.url());
1173
-
1174
- // Track tokens and call counts
1175
- let callCount = 0;
1176
- let cachedToken: string | null = null;
1177
-
1178
- const browserFetch = async (url: string, init?: RequestInit) => {
1179
- console.log(`browserFetch called for ${url}, callCount: ${callCount}`);
1180
- const response = await page.evaluate(
1181
- async ([fetchUrl, fetchInit]) => {
1182
- const res = await fetch(fetchUrl, {
1183
- ...fetchInit,
1184
- credentials: "include",
1185
- });
1186
- const body = await res.text();
1187
- return {
1188
- ok: res.ok,
1189
- status: res.status,
1190
- statusText: res.statusText,
1191
- headers: Object.fromEntries(res.headers.entries()),
1192
- body,
1193
- };
1194
- },
1195
- [url, init] as [string, RequestInit]
1196
- );
1197
-
1198
- console.log(`Raw response from ${url}:`, {
1199
- status: response.status,
1200
- statusText: response.statusText,
1201
- body: response.body,
1202
- });
1203
-
1204
- const fetchResponse = new Response(response.body || null, {
1205
- status: response.status,
1206
- statusText: response.statusText,
1207
- headers: response.headers,
1208
- });
1209
-
1210
- // Capture token from response
1211
- if (url.includes("/auth/temporary/")) {
1212
- const json = await fetchResponse.clone().json();
1213
- const token = json.temporaryAuthorization;
1214
- cachedToken = token; // Store the pre-fetched token
1215
- console.log(`Captured pre-fetched token: ${cachedToken}`);
1216
- }
1217
-
1218
- return fetchResponse;
1219
- };
1220
-
1221
- const client = quickbase({
1222
- realm,
1223
- useTempTokens: true,
1224
- debug: true,
1225
- fetchApi: async (url, init) => {
1226
- callCount++;
1227
- console.log(`fetchApi called for ${url}, callCount: ${callCount}`);
1228
-
1229
- if (url.includes("/auth/temporary/")) {
1230
- return browserFetch(url, init); // Use browser fetch for temp tokens
1231
- }
1232
-
1233
- // Use node-fetch for app calls, ensuring we track reuse
1234
- console.log(`Fetching ${url} with init:`, init);
1235
- const response = await fetch(url, init as RequestInit);
1236
- const body = await response.text();
1237
-
1238
- console.log(`Raw response from ${url}:`, {
1239
- status: response.status,
1240
- statusText: response.statusText,
1241
- body,
1242
- });
1243
-
1244
- return new Response(body || null, {
1245
- status: response.status,
1246
- statusText: response.statusText,
1247
- headers: Object.fromEntries(response.headers.entries()),
1248
- });
1249
- },
1250
- });
1251
-
1252
- // Step 1: Pre-fetch token
1253
- console.log("Pre-fetching token...");
1254
- const tokenResult = await client.getTempTokenDBID({ dbid: appId });
1255
- console.log("Pre-fetched token result:", tokenResult);
1256
- expect(tokenResult).toHaveProperty("temporaryAuthorization");
1257
- expect(cachedToken).toBe(tokenResult.temporaryAuthorization);
1258
-
1259
- // Step 2: Call getApp, expecting it to reuse the cached token
1260
- console.log("Calling getApp with pre-fetched token...");
1261
- const appResult = await client.getApp({ appId });
1262
- console.log("App response:", appResult);
1263
-
1264
- // Validate the app response
1265
- expect(appResult).toHaveProperty("id", appId);
1266
- expect(appResult).toHaveProperty("name");
1267
-
1268
- // Verify call count and token reuse
1269
- console.log(`Final callCount: ${callCount}`);
1270
- expect(callCount).toBe(2); // One for pre-fetch, one for getApp
1271
- expect(cachedToken).toBeDefined();
1272
-
1273
- // Check that getApp reused the token by inspecting logs manually or adding a header check if needed
1274
- console.log(`Pre-fetched token: ${cachedToken}`);
1275
- });
1276
- });
1277
750
  - name: tokenRenewal.test.ts
1278
751
  type: file
1279
- contents: |
1280
- // tests/playwright/qb/auth/tokenRenewal.test.ts
1281
- import { test, expect } from "@playwright/test";
1282
- import { quickbase } from "../../../../src/quickbaseClient.ts";
1283
- import dotenv from "dotenv";
1284
-
1285
- dotenv.config();
1286
-
1287
- const loginToQuickbase = async (
1288
- page,
1289
- quickbaseUrl,
1290
- username,
1291
- password,
1292
- appUrl
1293
- ) => {
1294
- console.log("Logging in to QuickBase");
1295
- await page.goto(quickbaseUrl, { timeout: 60000 });
1296
- await page.waitForSelector("input[name='loginid']", { timeout: 60000 });
1297
- await page.fill("input[name='loginid']", username);
1298
- await page.fill("input[name='password']", password);
1299
- await page.click("#signin");
1300
- await page.waitForURL(`https://*.quickbase.com/**`, {
1301
- waitUntil: "networkidle",
1302
- timeout: 60000,
1303
- });
1304
- console.log("Signed In to QuickBase.");
1305
- await page.goto(appUrl, { waitUntil: "networkidle", timeout: 60000 });
1306
- console.log("Navigated to app URL:", page.url());
1307
- };
1308
-
1309
- test.describe("QuickbaseClient Integration - Temp Token Renewal", () => {
1310
- test("renews temp token after 5 minutes with getApp → wait → getApp sequence", async ({
1311
- page,
1312
- }) => {
1313
- const realm = process.env.QB_REALM!;
1314
- const appId = process.env.QB_APP_ID!;
1315
- const username = process.env.QB_USERNAME!;
1316
- const password = process.env.QB_PASSWORD!;
1317
- const quickbaseUrl = `https://${realm}.quickbase.com/db/main?a=SignIn`;
1318
- const appUrl = `https://${realm}.quickbase.com/db/${appId}`;
1319
-
1320
- await loginToQuickbase(page, quickbaseUrl, username, password, appUrl);
1321
-
1322
- const browserFetch = async (
1323
- url: string,
1324
- init: RequestInit
1325
- ): Promise<Response> => {
1326
- const response = await page.request.fetch(url, {
1327
- method: init.method || "GET",
1328
- headers: {
1329
- ...init.headers,
1330
- Referer: `https://${realm}.quickbase.com/db/${appId}`,
1331
- Origin: `https://${realm}.quickbase.com`,
1332
- },
1333
- data: init.body,
1334
- credentials: "include",
1335
- });
1336
- const body = await response.text();
1337
- console.log(
1338
- "Response from:",
1339
- url,
1340
- "status:",
1341
- response.status(),
1342
- "body:",
1343
- body
1344
- );
1345
- return new Response(body, {
1346
- status: response.status(),
1347
- statusText: response.statusText(),
1348
- headers: response.headers(),
1349
- });
1350
- };
1351
-
1352
- const client = quickbase({
1353
- realm,
1354
- useTempTokens: true,
1355
- debug: true,
1356
- fetchApi: browserFetch,
1357
- });
1358
-
1359
- console.log("Checking session state before getApp...");
1360
- const cookies = await page.context().cookies();
1361
- const sessionCookie = cookies.find(
1362
- (c) => c.name === "TICKET" || c.name.startsWith("TICKET_")
1363
- );
1364
- console.log(
1365
- "Session cookie present:",
1366
- !!sessionCookie,
1367
- "Cookies:",
1368
- cookies
1369
- );
1370
- if (!sessionCookie) throw new Error("TICKET session cookie not found");
1371
-
1372
- await page.waitForTimeout(5000); // Debug delay
1373
- console.log("Cookies after delay:", await page.context().cookies());
1374
-
1375
- const appResult1 = await client.getApp({ appId });
1376
- console.log("First getApp response:", appResult1);
1377
- expect(appResult1.id).toBe(appId);
1378
-
1379
- console.log("Waiting 5 minutes to test token expiration...");
1380
- await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
1381
-
1382
- await page.reload({ waitUntil: "load", timeout: 60000 });
1383
- console.log("Refreshed page after 5 minutes, URL:", page.url());
1384
-
1385
- const appResult2 = await client.getApp({ appId });
1386
- console.log("Second getApp response:", appResult2);
1387
- expect(appResult2.id).toBe(appId);
1388
- });
1389
- });
1390
752
  - name: setup.ts
1391
753
  type: file
1392
- contents: |
1393
- // @tests/setup.ts
1394
- import { vi } from "vitest";
1395
- import { quickbase } from "../src/quickbaseClient.ts";
1396
- import type { QuickbaseConfig } from "../src/quickbaseClient.ts";
1397
-
1398
- export const mockFetch = vi.fn();
1399
-
1400
- export const createClient = (
1401
- fetchApi?: any,
1402
- config: Partial<QuickbaseConfig> = {}
1403
- ) => {
1404
- const client = quickbase({
1405
- realm: process.env.QB_REALM || "test-realm", // Match upsert.test.ts
1406
- userToken: process.env.QB_USER_TOKEN || "test-token", // Match upsert.test.ts
1407
- debug: true,
1408
- fetchApi,
1409
- ...config,
1410
- });
1411
- console.log("[createClient] Config:", config); // Debug config
1412
- console.log("[createClient] Returning:", client); // Debug client
1413
- return client;
1414
- };
1415
-
1416
- // Mock env variables for consistency
1417
- vi.stubEnv("QB_REALM", "builderprogram-dbradford6815");
1418
- vi.stubEnv("QB_USER_TOKEN", "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3");
1419
- vi.stubEnv("QB_APP_ID", "buwai2zpe");
1420
754
  - name: vitest
1421
755
  type: directory
1422
756
  children:
@@ -1428,696 +762,37 @@ tree:
1428
762
  children:
1429
763
  - name: getApp.test.ts
1430
764
  type: file
1431
- contents: |
1432
- import { test, expect } from "vitest";
1433
- import { quickbase } from "../../../../src/quickbaseClient";
1434
-
1435
- test(
1436
- "QuickbaseClient Integration - getApp > fetches real app data from QuickBase",
1437
- { timeout: 10000 },
1438
- async () => {
1439
- const config = {
1440
- realm: process.env.QB_REALM || "",
1441
- userToken: process.env.QB_USER_TOKEN || "",
1442
- debug: true,
1443
- };
1444
- const client = quickbase(config);
1445
- const getAppId = "buwai2zpe";
1446
-
1447
- console.log("Config used:", config);
1448
- const result = await client.getApp({ appId: getAppId });
1449
- console.log("Real API response:", result);
1450
-
1451
- expect(result).toEqual({
1452
- id: getAppId,
1453
- name: "qb-copy",
1454
- created: expect.any(Date), // Matches Date object from client
1455
- updated: expect.any(Date), // Matches Date object from client
1456
- description: "",
1457
- timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
1458
- dateFormat: "MM-DD-YYYY",
1459
- hasEveryoneOnTheInternet: false,
1460
- memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
1461
- securityProperties: {
1462
- allowClone: false,
1463
- allowExport: true,
1464
- enableAppTokens: true,
1465
- hideFromPublic: false,
1466
- mustBeRealmApproved: false,
1467
- useIPFilter: false,
1468
- },
1469
- });
1470
- }
1471
- );
1472
765
  - name: fields
1473
766
  type: directory
1474
767
  children:
1475
768
  - name: getFields.test.ts
1476
769
  type: file
1477
- contents: |
1478
- import { describe, it, expect } from "vitest";
1479
- import { createClient } from "../../../setup.ts";
1480
-
1481
- describe("QuickbaseClient Integration - getFields", () => {
1482
- const client = createClient();
1483
-
1484
- it(
1485
- "fetches real field data from QuickBase",
1486
- async () => {
1487
- const tableId = "buwai2z3s";
1488
- if (!tableId) throw new Error("Table ID is not defined");
1489
- if (!process.env.QB_REALM)
1490
- throw new Error("QB_REALM is not defined in .env");
1491
- if (!process.env.QB_USER_TOKEN)
1492
- throw new Error("QB_USER_TOKEN is not defined in .env");
1493
-
1494
- console.log("Config used:", {
1495
- realm: process.env.QB_REALM,
1496
- userToken: process.env.QB_USER_TOKEN,
1497
- tableId,
1498
- });
1499
- const result = await client.getFields({
1500
- tableId,
1501
- includeFieldPerms: true,
1502
- });
1503
- console.log("Real API response:", result);
1504
- expect(result).toEqual(
1505
- expect.arrayContaining([
1506
- expect.objectContaining({
1507
- id: 1,
1508
- label: "Date Created",
1509
- fieldType: "timestamp",
1510
- noWrap: true,
1511
- bold: false,
1512
- required: false,
1513
- appearsByDefault: false,
1514
- findEnabled: false,
1515
- unique: false,
1516
- doesDataCopy: false,
1517
- fieldHelp: "",
1518
- audited: false,
1519
- properties: expect.objectContaining({
1520
- primaryKey: false,
1521
- foreignKey: false,
1522
- sortAsGiven: true,
1523
- carryChoices: true,
1524
- allowNewChoices: false,
1525
- formula: "",
1526
- defaultValue: "",
1527
- }),
1528
- permissions: expect.arrayContaining([
1529
- expect.objectContaining({
1530
- permissionType: "Modify",
1531
- role: "Viewer",
1532
- roleId: 10,
1533
- }),
1534
- expect.objectContaining({
1535
- permissionType: "Modify",
1536
- role: "Participant",
1537
- roleId: 11,
1538
- }),
1539
- expect.objectContaining({
1540
- permissionType: "Modify",
1541
- role: "Administrator",
1542
- roleId: 12,
1543
- }),
1544
- ]),
1545
- }),
1546
- expect.objectContaining({
1547
- id: 2,
1548
- label: "Date Modified",
1549
- fieldType: "timestamp",
1550
- noWrap: true,
1551
- bold: false,
1552
- required: false,
1553
- appearsByDefault: false,
1554
- findEnabled: false,
1555
- unique: false,
1556
- doesDataCopy: false,
1557
- fieldHelp: "",
1558
- audited: false,
1559
- properties: expect.objectContaining({
1560
- primaryKey: false,
1561
- foreignKey: false,
1562
- sortAsGiven: true,
1563
- carryChoices: true,
1564
- allowNewChoices: false,
1565
- formula: "",
1566
- defaultValue: "",
1567
- }),
1568
- permissions: expect.arrayContaining([
1569
- expect.objectContaining({
1570
- permissionType: "Modify",
1571
- role: "Viewer",
1572
- roleId: 10,
1573
- }),
1574
- expect.objectContaining({
1575
- permissionType: "Modify",
1576
- role: "Participant",
1577
- roleId: 11,
1578
- }),
1579
- expect.objectContaining({
1580
- permissionType: "Modify",
1581
- role: "Administrator",
1582
- roleId: 12,
1583
- }),
1584
- ]),
1585
- }),
1586
- expect.objectContaining({
1587
- id: 3,
1588
- label: "Record ID#",
1589
- fieldType: "recordid",
1590
- noWrap: true,
1591
- bold: false,
1592
- required: false,
1593
- appearsByDefault: false,
1594
- findEnabled: false,
1595
- unique: true,
1596
- doesDataCopy: false,
1597
- fieldHelp: "",
1598
- audited: false,
1599
- properties: expect.objectContaining({
1600
- primaryKey: true,
1601
- foreignKey: false,
1602
- sortAsGiven: true,
1603
- carryChoices: true,
1604
- allowNewChoices: false,
1605
- formula: "",
1606
- defaultValue: "",
1607
- }),
1608
- permissions: expect.arrayContaining([
1609
- expect.objectContaining({
1610
- permissionType: "Modify",
1611
- role: "Viewer",
1612
- roleId: 10,
1613
- }),
1614
- expect.objectContaining({
1615
- permissionType: "Modify",
1616
- role: "Participant",
1617
- roleId: 11,
1618
- }),
1619
- expect.objectContaining({
1620
- permissionType: "Modify",
1621
- role: "Administrator",
1622
- roleId: 12,
1623
- }),
1624
- ]),
1625
- }),
1626
- expect.objectContaining({
1627
- id: 4,
1628
- label: "Record Owner",
1629
- fieldType: "user",
1630
- noWrap: true,
1631
- bold: false,
1632
- required: false,
1633
- appearsByDefault: false,
1634
- findEnabled: true,
1635
- unique: false,
1636
- doesDataCopy: false,
1637
- fieldHelp: "",
1638
- audited: false,
1639
- properties: expect.objectContaining({
1640
- primaryKey: false,
1641
- foreignKey: false,
1642
- sortAsGiven: true,
1643
- carryChoices: true,
1644
- allowNewChoices: true,
1645
- formula: "",
1646
- defaultValue: "",
1647
- }),
1648
- permissions: expect.arrayContaining([
1649
- expect.objectContaining({
1650
- permissionType: "Modify",
1651
- role: "Viewer",
1652
- roleId: 10,
1653
- }),
1654
- expect.objectContaining({
1655
- permissionType: "Modify",
1656
- role: "Participant",
1657
- roleId: 11,
1658
- }),
1659
- expect.objectContaining({
1660
- permissionType: "Modify",
1661
- role: "Administrator",
1662
- roleId: 12,
1663
- }),
1664
- ]),
1665
- }),
1666
- expect.objectContaining({
1667
- id: 5,
1668
- label: "Last Modified By",
1669
- fieldType: "user",
1670
- noWrap: true,
1671
- bold: false,
1672
- required: false,
1673
- appearsByDefault: false,
1674
- findEnabled: true,
1675
- unique: false,
1676
- doesDataCopy: false,
1677
- fieldHelp: "",
1678
- audited: false,
1679
- properties: expect.objectContaining({
1680
- primaryKey: false,
1681
- foreignKey: false,
1682
- sortAsGiven: true,
1683
- carryChoices: true,
1684
- allowNewChoices: true,
1685
- formula: "",
1686
- defaultValue: "",
1687
- }),
1688
- permissions: expect.arrayContaining([
1689
- expect.objectContaining({
1690
- permissionType: "Modify",
1691
- role: "Viewer",
1692
- roleId: 10,
1693
- }),
1694
- expect.objectContaining({
1695
- permissionType: "Modify",
1696
- role: "Participant",
1697
- roleId: 11,
1698
- }),
1699
- expect.objectContaining({
1700
- permissionType: "Modify",
1701
- role: "Administrator",
1702
- roleId: 12,
1703
- }),
1704
- ]),
1705
- }),
1706
- ])
1707
- );
1708
- },
1709
- { timeout: 10000 }
1710
- );
1711
- });
1712
770
  - name: index.ts
1713
771
  type: file
1714
- contents: |
1715
- // Optional: Only add if you need to export tests
1716
- export * from "./apps/getApp.test.ts";
1717
- export * from "./fields/getFields.test.ts";
1718
- export * from "./tables/getAppTables.test.ts";
1719
772
  - name: records
1720
773
  type: directory
1721
774
  children:
1722
775
  - name: deleteRecords.test.ts
1723
776
  type: file
1724
- contents: |
1725
- // tests/vitest/qb/records/deleteRecords.test.ts
1726
- import { describe, test, expect, beforeEach, afterEach } from "vitest";
1727
- import { quickbase } from "@/quickbaseClient.ts";
1728
-
1729
- const TABLE_ID = "buwai2zws";
1730
- const UNIQUE_FIELD_ID = 3; // Record ID field
1731
- const MARKER_FIELD_ID = 6; // Text field for marking test records
1732
-
1733
- describe("QuickbaseClient - DeleteRecords Integration", () => {
1734
- const qb = quickbase({
1735
- realm: "builderprogram-dbradford6815",
1736
- userToken: process.env.QB_USER_TOKEN,
1737
- debug: true,
1738
- });
1739
-
1740
- beforeEach(async () => {
1741
- try {
1742
- await qb.deleteRecords({
1743
- body: {
1744
- from: TABLE_ID,
1745
- where: `{${MARKER_FIELD_ID}.CT.'test-marker-delete'}`,
1746
- },
1747
- });
1748
- } catch (e) {
1749
- // Ignore if no records exist
1750
- }
1751
- });
1752
-
1753
- afterEach(async () => {
1754
- try {
1755
- await qb.deleteRecords({
1756
- body: {
1757
- from: TABLE_ID,
1758
- where: `{${MARKER_FIELD_ID}.CT.'test-marker-delete'}`,
1759
- },
1760
- });
1761
- } catch (e) {}
1762
- });
1763
-
1764
- test(
1765
- "deletes a newly upserted record",
1766
- async () => {
1767
- const upsertResponse = await qb.upsert({
1768
- body: {
1769
- to: TABLE_ID,
1770
- data: [{ [MARKER_FIELD_ID]: { value: "test-marker-delete" } }],
1771
- fieldsToReturn: [UNIQUE_FIELD_ID], // Return record ID
1772
- },
1773
- });
1774
- const recordId = upsertResponse.metadata.createdRecordIds[0];
1775
- expect(recordId).toBeDefined();
1776
- expect(upsertResponse.data[0][UNIQUE_FIELD_ID].value).toBe(recordId);
1777
-
1778
- const deleteResponse = await qb.deleteRecords({
1779
- body: { from: TABLE_ID, where: `{${UNIQUE_FIELD_ID}.EX.${recordId}}` },
1780
- });
1781
-
1782
- expect(deleteResponse).toEqual({ numberDeleted: 1 });
1783
-
1784
- const checkResponse = await qb.deleteRecords({
1785
- body: { from: TABLE_ID, where: `{${UNIQUE_FIELD_ID}.EX.${recordId}}` },
1786
- });
1787
- expect(checkResponse.numberDeleted).toBe(0);
1788
- },
1789
- { timeout: 10000 }
1790
- );
1791
-
1792
- test(
1793
- "handles empty result when no records match",
1794
- async () => {
1795
- const response = await qb.deleteRecords({
1796
- body: { from: TABLE_ID, where: "{3.EX.'999999'}" },
1797
- });
1798
- expect(response).toEqual({ numberDeleted: 0 });
1799
- },
1800
- { timeout: 10000 }
1801
- );
1802
- });
777
+ - name: runQuery.test.ts
778
+ type: file
1803
779
  - name: upsert.test.ts
1804
780
  type: file
1805
- contents: |
1806
- // tests/vitest/qb/records/upsert.test.ts
1807
- import { describe, test, expect, beforeEach, afterEach } from "vitest";
1808
- import { quickbase } from "@/quickbaseClient.ts";
1809
-
1810
- const TABLE_ID = "buwai2zws";
1811
- const MARKER_FIELD_ID = 6; // Text field for marking test records
1812
-
1813
- describe("QuickbaseClient - Upsert Integration", () => {
1814
- const qb = quickbase({
1815
- realm: "builderprogram-dbradford6815",
1816
- userToken: process.env.QB_USER_TOKEN,
1817
- debug: true,
1818
- });
1819
-
1820
- beforeEach(async () => {
1821
- try {
1822
- await qb.deleteRecords({
1823
- body: {
1824
- from: TABLE_ID,
1825
- where: `{${MARKER_FIELD_ID}.CT.'test-marker-upsert'}`,
1826
- },
1827
- });
1828
- } catch (e) {
1829
- // Ignore if no records exist
1830
- }
1831
- });
1832
-
1833
- afterEach(async () => {
1834
- try {
1835
- await qb.deleteRecords({
1836
- body: {
1837
- from: TABLE_ID,
1838
- where: `{${MARKER_FIELD_ID}.CT.'test-marker-upsert'}`,
1839
- },
1840
- });
1841
- } catch (e) {}
1842
- });
1843
-
1844
- test(
1845
- "creates a new record successfully",
1846
- async () => {
1847
- const response = await qb.upsert({
1848
- body: {
1849
- to: TABLE_ID,
1850
- data: [{ [MARKER_FIELD_ID]: { value: "test-marker-upsert" } }],
1851
- fieldsToReturn: [MARKER_FIELD_ID, 3], // Return marker and record ID
1852
- },
1853
- });
1854
-
1855
- expect(response.metadata.createdRecordIds.length).toBe(1);
1856
- expect(response.data).toHaveLength(1);
1857
- expect(response.data[0][MARKER_FIELD_ID].value).toBe(
1858
- "test-marker-upsert"
1859
- );
1860
- expect(response.data[0][3].value).toBeDefined(); // Record ID assigned by QuickBase
1861
- expect(response.metadata.totalNumberOfRecordsProcessed).toBe(1);
1862
- },
1863
- { timeout: 10000 }
1864
- );
1865
-
1866
- test(
1867
- "handles empty data gracefully",
1868
- async () => {
1869
- await expect(
1870
- qb.upsert({
1871
- body: { to: TABLE_ID, data: [] },
1872
- })
1873
- ).rejects.toThrow("API Error: Bad Request (Status: 400)");
1874
- },
1875
- { timeout: 10000 }
1876
- );
1877
- });
1878
781
  - name: tables
1879
782
  type: directory
1880
783
  children:
1881
784
  - name: createTable.test.ts
1882
785
  type: file
1883
- contents: |
1884
- import { test, expect } from "vitest";
1885
- import { quickbase } from "../../../../src/quickbaseClient";
1886
-
1887
- test(
1888
- "QuickbaseClient Integration - createTable > creates a new table in QuickBase",
1889
- { timeout: 10000 },
1890
- async () => {
1891
- const config = {
1892
- realm: process.env.QB_REALM || "",
1893
- userToken: process.env.QB_USER_TOKEN || "",
1894
- debug: true,
1895
- };
1896
- const client = quickbase(config);
1897
- const appId = "buwai2zpe";
1898
-
1899
- const tableName = `TestTable_${Date.now()}`;
1900
- const createBody = {
1901
- name: tableName,
1902
- description: "Test table creation",
1903
- singleRecordName: "TestRecord",
1904
- pluralRecordName: "TestRecords",
1905
- };
1906
-
1907
- console.log("Config used:", config);
1908
- console.log("Creating table with:", createBody);
1909
- const response = await client.createTable({ appId, body: createBody });
1910
-
1911
- expect(response).toBeDefined();
1912
- expect(response.id).toBeDefined();
1913
- expect(response.name).toBe(tableName);
1914
- expect(response.description).toBe(createBody.description);
1915
- expect(response.singleRecordName).toBe(createBody.singleRecordName);
1916
- expect(response.pluralRecordName).toBe(createBody.pluralRecordName);
1917
- expect(response.alias).toMatch(/^_DBID_/);
1918
- expect(response.created).toBeInstanceOf(Date);
1919
- expect(response.updated).toBeInstanceOf(Date);
1920
- expect(response.nextRecordId).toBeGreaterThanOrEqual(1);
1921
- expect(response.nextFieldId).toBeGreaterThanOrEqual(1);
1922
- expect(response.defaultSortFieldId).toBeGreaterThanOrEqual(0);
1923
- expect(response.keyFieldId).toBeGreaterThanOrEqual(1);
1924
- expect(response.sizeLimit).toMatch(/^\d+\sMB$/); // e.g., "500 MB"
1925
- expect(response.spaceUsed).toMatch(/^\d+\s(KB|MB)$/); // e.g., "0 KB"
1926
- expect(response.spaceRemaining).toMatch(/^\d+\sMB$/); // e.g., "500 MB"
1927
-
1928
- console.log("Real API response:", response);
1929
-
1930
- // Cleanup: Delete the table to avoid clutter
1931
- const deleteResponse = await client.deleteTable({
1932
- tableId: response.id,
1933
- appId,
1934
- });
1935
- expect(deleteResponse.deletedTableId).toBe(response.id);
1936
- console.log("Cleanup - Deleted table:", deleteResponse);
1937
- }
1938
- );
1939
786
  - name: deleteTable.test.ts
1940
787
  type: file
1941
- contents: |
1942
- import { test, expect } from "vitest";
1943
- import { quickbase } from "../../../../src/quickbaseClient";
1944
-
1945
- test(
1946
- "QuickbaseClient Integration - deleteTable > deletes a table in QuickBase",
1947
- { timeout: 20000 },
1948
- async () => {
1949
- const config = {
1950
- realm: process.env.QB_REALM || "",
1951
- userToken: process.env.QB_USER_TOKEN || "",
1952
- debug: true,
1953
- };
1954
- const client = quickbase(config);
1955
- const appId = "buwai2zpe";
1956
-
1957
- // Step 1: Create a temporary table to delete
1958
- const tableName = `DeleteTest_${Date.now()}`;
1959
- const createBody = {
1960
- name: tableName,
1961
- description: "Temporary table for delete test",
1962
- singleRecordName: "DeleteTest",
1963
- pluralRecordName: "DeleteTests",
1964
- };
1965
- console.log("Creating temporary table with:", createBody);
1966
- const createResponse = await client.createTable({
1967
- appId,
1968
- body: createBody,
1969
- });
1970
- expect(createResponse.id).toBeDefined();
1971
- expect(createResponse.name).toBe(tableName);
1972
- console.log("Created table:", createResponse);
1973
- const tableId = createResponse.id;
1974
-
1975
- // Step 2: Delete the table
1976
- console.log("Deleting table:", { tableId, appId });
1977
- const deleteResponse = await client.deleteTable({ tableId, appId });
1978
- expect(deleteResponse).toBeDefined();
1979
- expect(deleteResponse.deletedTableId).toBe(tableId);
1980
- console.log("Delete API response:", deleteResponse);
1981
-
1982
- // Step 3: Verify the table is gone
1983
- try {
1984
- await client.getTable({ tableId, appId });
1985
- throw new Error("Table should not exist after deletion");
1986
- } catch (error) {
1987
- expect(error.message).toMatch(/404|not found/i); // Expect a 404 error
1988
- console.log("Verified table deletion:", error.message);
1989
- }
1990
- }
1991
- );
1992
788
  - name: getAppTables.test.ts
1993
789
  type: file
1994
- contents: |
1995
- import { test, expect } from "vitest";
1996
- import { quickbase } from "../../../../src/quickbaseClient";
1997
-
1998
- test(
1999
- "QuickbaseClient Integration - getAppTables > fetches real table data from QuickBase",
2000
- { timeout: 10000 },
2001
- async () => {
2002
- const config = {
2003
- realm: process.env.QB_REALM || "",
2004
- userToken: process.env.QB_USER_TOKEN || "",
2005
- debug: true,
2006
- };
2007
- const client = quickbase(config);
2008
- const tablesAppId = "buwai2zpe";
2009
-
2010
- console.log("Config used:", config);
2011
- const result = await client.getAppTables({ appId: tablesAppId });
2012
- console.log("Real API response:", result);
2013
-
2014
- expect(result).toBeDefined();
2015
- expect(result).toBeInstanceOf(Array);
2016
- expect(result.length).toBeGreaterThan(0);
2017
- expect(result).toEqual(
2018
- expect.arrayContaining([
2019
- expect.objectContaining({
2020
- id: "buwai2zr4",
2021
- alias: "_DBID_ROOT",
2022
- name: expect.any(String), // Allow dynamic name due to updateTable
2023
- description: expect.any(String), // Allow dynamic description
2024
- created: expect.any(Date),
2025
- updated: expect.any(Date), // Allow varying timestamps
2026
- nextRecordId: expect.any(Number),
2027
- nextFieldId: expect.any(Number),
2028
- defaultSortFieldId: expect.any(Number),
2029
- defaultSortOrder: "DESC",
2030
- keyFieldId: expect.any(Number),
2031
- singleRecordName: "Root",
2032
- pluralRecordName: "Roots",
2033
- sizeLimit: "500 MB",
2034
- spaceUsed: expect.any(String),
2035
- spaceRemaining: "500 MB",
2036
- }),
2037
- ])
2038
- );
2039
- }
2040
- );
790
+ - name: getRelationships.test.ts
791
+ type: file
2041
792
  - name: getTable.test.ts
2042
793
  type: file
2043
- contents: |
2044
- import { test, expect } from "vitest";
2045
- import { quickbase } from "../../../../src/quickbaseClient";
2046
-
2047
- test(
2048
- "QuickbaseClient Integration - getTable > fetches real table data from QuickBase",
2049
- { timeout: 10000 },
2050
- async () => {
2051
- const config = {
2052
- realm: process.env.QB_REALM || "",
2053
- userToken: process.env.QB_USER_TOKEN || "",
2054
- debug: true,
2055
- };
2056
- const client = quickbase(config);
2057
- const tableId = "buwai2zr4";
2058
- const appId = "buwai2zpe";
2059
-
2060
- console.log("Config used:", config);
2061
- const response = await client.getTable({ tableId, appId });
2062
- expect(response).toBeDefined();
2063
- expect(response.id).toBe(tableId);
2064
- expect(response.name).toBe("Root"); // Expect original name
2065
- expect(response.alias).toBe("_DBID_ROOT");
2066
- expect(response.created).toBeInstanceOf(Date);
2067
- console.log("Real API response:", response);
2068
- }
2069
- );
2070
794
  - name: updateTable.test.ts
2071
795
  type: file
2072
- contents: |
2073
- import { test, expect } from "vitest";
2074
- import { quickbase } from "../../../../src/quickbaseClient";
2075
-
2076
- test(
2077
- "QuickbaseClient Integration - updateTable > updates real table data in QuickBase",
2078
- { timeout: 10000 },
2079
- async () => {
2080
- const config = {
2081
- realm: process.env.QB_REALM || "",
2082
- userToken: process.env.QB_USER_TOKEN || "",
2083
- debug: true,
2084
- };
2085
- const client = quickbase(config);
2086
- const appId = "buwai2zpe";
2087
- const tableId = "buwai2zr4";
2088
- const updatedBody = {
2089
- name: "Root Updated " + Date.now(),
2090
- description: "Integration test update",
2091
- };
2092
- const originalBody = {
2093
- name: "Root",
2094
- description: "",
2095
- };
2096
-
2097
- console.log("Config used:", config);
2098
- // Update the table
2099
- const response = await client.updateTable({
2100
- tableId,
2101
- appId,
2102
- body: updatedBody,
2103
- });
2104
- expect(response).toBeDefined();
2105
- expect(response.id).toBe(tableId);
2106
- expect(response.name).toBe(updatedBody.name);
2107
- expect(response.description).toBe(updatedBody.description);
2108
- console.log("Real API response:", response);
2109
-
2110
- // Cleanup: Revert the table to original state
2111
- const cleanupResponse = await client.updateTable({
2112
- tableId,
2113
- appId,
2114
- body: originalBody,
2115
- });
2116
- expect(cleanupResponse.name).toBe(originalBody.name);
2117
- expect(cleanupResponse.description).toBe(originalBody.description);
2118
- console.log("Cleanup API response:", cleanupResponse);
2119
- }
2120
- );
2121
796
  - name: unit
2122
797
  type: directory
2123
798
  children:
@@ -2127,14 +802,22 @@ tree:
2127
802
  - name: getApp.test.ts
2128
803
  type: file
2129
804
  contents: |
805
+ // tests/vitest/unit/apps/getApp.test.ts
2130
806
  import { describe, it, expect, beforeEach } from "vitest";
2131
- import { createClient, mockFetch } from "@tests/setup.ts";
2132
-
2133
- describe("QuickbaseClient - getApp (Unit)", () => {
2134
- const client = createClient(mockFetch);
807
+ import {
808
+ createClient,
809
+ mockFetch,
810
+ QB_APP_ID,
811
+ QB_REALM,
812
+ QB_USER_TOKEN,
813
+ } from "@tests/setup.ts";
814
+
815
+ describe("QuickbaseClient Unit - getApp", () => {
816
+ let client: ReturnType<typeof createClient>;
2135
817
 
2136
818
  beforeEach(() => {
2137
819
  mockFetch.mockClear();
820
+ client = createClient(mockFetch, { debug: true }); // Add debug: true for consistency
2138
821
  });
2139
822
 
2140
823
  it("initializes without errors", () => {
@@ -2153,7 +836,7 @@ tree:
2153
836
  status: 200,
2154
837
  json: () =>
2155
838
  Promise.resolve({
2156
- id: process.env.QB_APP_ID,
839
+ id: QB_APP_ID,
2157
840
  name: "qb-copy",
2158
841
  created: "2025-02-13T18:22:33Z",
2159
842
  updated: "2025-03-04T04:25:51Z",
@@ -2174,12 +857,9 @@ tree:
2174
857
  } as Response);
2175
858
  });
2176
859
 
2177
- const getAppId = process.env.QB_APP_ID;
2178
- if (!getAppId) throw new Error("QB_APP_ID is not defined in .env");
2179
- console.log("Test appId:", getAppId);
2180
- const result = await client.getApp({ appId: getAppId });
860
+ const result = await client.getApp({ appId: QB_APP_ID });
2181
861
  expect(result).toEqual({
2182
- id: getAppId,
862
+ id: QB_APP_ID,
2183
863
  name: "qb-copy",
2184
864
  created: new Date("2025-02-13T18:22:33Z"),
2185
865
  updated: new Date("2025-03-04T04:25:51Z"),
@@ -2198,11 +878,11 @@ tree:
2198
878
  },
2199
879
  });
2200
880
  expect(mockFetch).toHaveBeenCalledWith(
2201
- `https://api.quickbase.com/v1/apps/${getAppId}`,
881
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
2202
882
  expect.objectContaining({
2203
883
  headers: expect.objectContaining({
2204
- Authorization: `QB-USER-TOKEN ${process.env.QB_USER_TOKEN}`,
2205
- "QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
884
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
885
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2206
886
  }),
2207
887
  })
2208
888
  );
@@ -2214,10 +894,16 @@ tree:
2214
894
  - name: fetchTempToken401.test.ts
2215
895
  type: file
2216
896
  contents: |
897
+ // tests/vitest/unit/auth/fetchTempToken401.test.ts
2217
898
  import { describe, it, expect, beforeEach, vi } from "vitest";
2218
- import { createClient, mockFetch } from "@tests/setup.ts";
2219
-
2220
- describe("QuickbaseClient - 401 with fetchTempToken 401", () => {
899
+ import {
900
+ createClient,
901
+ mockFetch,
902
+ QB_REALM,
903
+ QB_TABLE_ID_1,
904
+ } from "@tests/setup.ts";
905
+
906
+ describe("QuickbaseClient Unit - 401 with fetchTempToken 401", () => {
2221
907
  let client: ReturnType<typeof createClient>;
2222
908
 
2223
909
  beforeEach(() => {
@@ -2226,7 +912,6 @@ tree:
2226
912
  });
2227
913
 
2228
914
  it("fails without infinite loop when fetchTempToken returns 401 after initial 401", async () => {
2229
- const mockDbid = "mockDbid123";
2230
915
  let callCount = 0;
2231
916
 
2232
917
  mockFetch.mockImplementation((url) => {
@@ -2259,11 +944,31 @@ tree:
2259
944
  });
2260
945
 
2261
946
  const consoleSpy = vi.spyOn(console, "log");
2262
- await expect(client.getFields({ tableId: mockDbid })).rejects.toThrow(
947
+ await expect(client.getFields({ tableId: QB_TABLE_ID_1 })).rejects.toThrow(
2263
948
  "API Error: Unauthorized in fetchTempToken (Status: 401)"
2264
949
  );
2265
950
 
2266
951
  expect(mockFetch).toHaveBeenCalledTimes(3);
952
+ expect(mockFetch).toHaveBeenNthCalledWith(
953
+ 1,
954
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
955
+ expect.any(Object)
956
+ );
957
+ expect(mockFetch).toHaveBeenNthCalledWith(
958
+ 2,
959
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`, // Removed &includeFieldPerms=false
960
+ expect.objectContaining({
961
+ headers: expect.objectContaining({
962
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
963
+ }),
964
+ })
965
+ );
966
+ expect(mockFetch).toHaveBeenNthCalledWith(
967
+ 3,
968
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
969
+ expect.any(Object)
970
+ );
971
+
2267
972
  expect(consoleSpy).toHaveBeenCalledWith(
2268
973
  "Authorization error for getFields, refreshing token:",
2269
974
  expect.any(String)
@@ -2277,10 +982,16 @@ tree:
2277
982
  - name: getTempToken.test.ts
2278
983
  type: file
2279
984
  contents: |
985
+ // tests/vitest/unit/auth/getTempTokenDBID.test.ts
2280
986
  import { describe, it, expect, beforeEach } from "vitest";
2281
- import { createClient, mockFetch } from "@tests/setup.ts";
2282
-
2283
- describe("QuickbaseClient - getTempTokenDBID (Unit)", () => {
987
+ import {
988
+ createClient,
989
+ mockFetch,
990
+ QB_REALM,
991
+ QB_TABLE_ID_1,
992
+ } from "@tests/setup.ts";
993
+
994
+ describe("QuickbaseClient Unit - getTempTokenDBID", () => {
2284
995
  let client: ReturnType<typeof createClient>;
2285
996
 
2286
997
  beforeEach(() => {
@@ -2297,7 +1008,6 @@ tree:
2297
1008
  });
2298
1009
 
2299
1010
  it("fetches and caches temp token on first call", async () => {
2300
- const mockDbid = "mockDbid123";
2301
1011
  const mockToken = "b123xyz_temp_token";
2302
1012
 
2303
1013
  mockFetch.mockResolvedValueOnce({
@@ -2306,14 +1016,14 @@ tree:
2306
1016
  json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
2307
1017
  });
2308
1018
 
2309
- const result = await client.getTempTokenDBID({ dbid: mockDbid });
1019
+ const result = await client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 });
2310
1020
  expect(result).toEqual({ temporaryAuthorization: mockToken });
2311
1021
  expect(mockFetch).toHaveBeenCalledWith(
2312
- `https://api.quickbase.com/v1/auth/temporary/${mockDbid}`,
1022
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
2313
1023
  expect.objectContaining({
2314
1024
  method: "GET",
2315
1025
  headers: expect.objectContaining({
2316
- "QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
1026
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2317
1027
  "Content-Type": "application/json",
2318
1028
  }),
2319
1029
  credentials: "include",
@@ -2322,7 +1032,6 @@ tree:
2322
1032
  });
2323
1033
 
2324
1034
  it("reuses cached temp token on second call", async () => {
2325
- const mockDbid = "mockDbid123";
2326
1035
  const mockToken = "b123xyz_temp_token";
2327
1036
 
2328
1037
  mockFetch.mockResolvedValueOnce({
@@ -2331,41 +1040,56 @@ tree:
2331
1040
  json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
2332
1041
  });
2333
1042
 
2334
- const firstResult = await client.getTempTokenDBID({ dbid: mockDbid });
1043
+ const firstResult = await client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 });
2335
1044
  expect(firstResult).toEqual({ temporaryAuthorization: mockToken });
2336
1045
  expect(mockFetch).toHaveBeenCalledTimes(1);
2337
1046
 
2338
1047
  mockFetch.mockClear();
2339
- const secondResult = await client.getTempTokenDBID({ dbid: mockDbid });
1048
+ const secondResult = await client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 });
2340
1049
  expect(secondResult).toEqual({ temporaryAuthorization: mockToken });
2341
1050
  expect(mockFetch).not.toHaveBeenCalled();
2342
1051
  });
2343
1052
 
2344
1053
  it("handles API error", async () => {
2345
- const mockDbid = "mockDbid123";
2346
-
2347
1054
  mockFetch.mockResolvedValueOnce({
2348
1055
  ok: false,
2349
1056
  status: 401,
2350
1057
  json: () => Promise.resolve({ message: "Unauthorized" }),
2351
1058
  });
2352
1059
 
2353
- await expect(client.getTempTokenDBID({ dbid: mockDbid })).rejects.toSatisfy(
2354
- (error: Error) => {
2355
- expect(error).toBeInstanceOf(Error);
2356
- expect(error.message).toBe("API Error: Unauthorized (Status: 401)");
2357
- return true;
2358
- }
1060
+ await expect(
1061
+ client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 })
1062
+ ).rejects.toSatisfy((error: Error) => {
1063
+ expect(error).toBeInstanceOf(Error);
1064
+ expect(error.message).toBe("API Error: Unauthorized (Status: 401)");
1065
+ return true;
1066
+ });
1067
+ expect(mockFetch).toHaveBeenCalledWith(
1068
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
1069
+ expect.objectContaining({
1070
+ method: "GET",
1071
+ headers: expect.objectContaining({
1072
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1073
+ "Content-Type": "application/json",
1074
+ }),
1075
+ credentials: "include",
1076
+ })
2359
1077
  );
2360
1078
  });
2361
1079
  });
2362
1080
  - name: retryOn401.test.ts
2363
1081
  type: file
2364
1082
  contents: |
1083
+ // tests/vitest/unit/auth/retryOn401.test.ts
2365
1084
  import { describe, it, expect, beforeEach, vi } from "vitest";
2366
- import { createClient, mockFetch } from "@tests/setup.ts";
2367
-
2368
- describe("QuickbaseClient - 401 Retry Creates New Token", () => {
1085
+ import {
1086
+ createClient,
1087
+ mockFetch,
1088
+ QB_REALM,
1089
+ QB_TABLE_ID_1,
1090
+ } from "@tests/setup.ts";
1091
+
1092
+ describe("QuickbaseClient Unit - 401 Retry Creates New Token", () => {
2369
1093
  let client: ReturnType<typeof createClient>;
2370
1094
 
2371
1095
  beforeEach(() => {
@@ -2374,7 +1098,6 @@ tree:
2374
1098
  });
2375
1099
 
2376
1100
  it("creates a new token on 401 and retries successfully", async () => {
2377
- const mockDbid = "mockDbid123";
2378
1101
  const mockToken = "new_token_456";
2379
1102
  const mockFields = [{ id: 1, label: "Field1" }];
2380
1103
  let callCount = 0;
@@ -2415,16 +1138,45 @@ tree:
2415
1138
  });
2416
1139
 
2417
1140
  const consoleSpy = vi.spyOn(console, "log");
2418
- const result = await client.getFields({ tableId: mockDbid });
1141
+ const result = await client.getFields({ tableId: QB_TABLE_ID_1 });
2419
1142
 
2420
1143
  expect(result).toEqual(mockFields);
2421
1144
  expect(mockFetch).toHaveBeenCalledTimes(4);
1145
+ expect(mockFetch).toHaveBeenNthCalledWith(
1146
+ 1,
1147
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
1148
+ expect.any(Object)
1149
+ );
1150
+ expect(mockFetch).toHaveBeenNthCalledWith(
1151
+ 2,
1152
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`, // Removed &includeFieldPerms=false
1153
+ expect.objectContaining({
1154
+ headers: expect.objectContaining({
1155
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1156
+ }),
1157
+ })
1158
+ );
1159
+ expect(mockFetch).toHaveBeenNthCalledWith(
1160
+ 3,
1161
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
1162
+ expect.any(Object)
1163
+ );
1164
+ expect(mockFetch).toHaveBeenNthCalledWith(
1165
+ 4,
1166
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`, // Removed &includeFieldPerms=false
1167
+ expect.objectContaining({
1168
+ headers: expect.objectContaining({
1169
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1170
+ }),
1171
+ })
1172
+ );
1173
+
2422
1174
  expect(consoleSpy).toHaveBeenCalledWith(
2423
1175
  "Authorization error for getFields, refreshing token:",
2424
1176
  expect.any(String)
2425
1177
  );
2426
1178
  expect(consoleSpy).toHaveBeenCalledWith(
2427
- "Fetched and cached new token for dbid: mockDbid123",
1179
+ `Fetched and cached new token for dbid: ${QB_TABLE_ID_1}`,
2428
1180
  mockToken + "_retry",
2429
1181
  expect.any(String)
2430
1182
  );
@@ -2520,10 +1272,17 @@ tree:
2520
1272
  - name: getFields.test.ts
2521
1273
  type: file
2522
1274
  contents: |
1275
+ // tests/vitest/unit/fields/getFields.test.ts
2523
1276
  import { describe, it, expect } from "vitest";
2524
- import { createClient, mockFetch } from "@tests/setup.ts";
2525
-
2526
- describe("QuickbaseClient - getFields (Unit)", () => {
1277
+ import {
1278
+ createClient,
1279
+ mockFetch,
1280
+ QB_REALM,
1281
+ QB_USER_TOKEN,
1282
+ QB_TABLE_ID_1,
1283
+ } from "@tests/setup.ts";
1284
+
1285
+ describe("QuickbaseClient Unit - getFields", () => {
2527
1286
  it("calls getFields successfully", async () => {
2528
1287
  mockFetch.mockImplementation((url: string, options: any) => {
2529
1288
  console.log("Mock fetch for getFields:", url, options);
@@ -2569,9 +1328,9 @@ tree:
2569
1328
  } as Response);
2570
1329
  });
2571
1330
 
2572
- const client = createClient(mockFetch);
1331
+ const client = createClient(mockFetch, { debug: true }); // Add debug: true for consistency
2573
1332
  const result = await client.getFields({
2574
- tableId: "dummyTableId",
1333
+ tableId: QB_TABLE_ID_1,
2575
1334
  includeFieldPerms: true,
2576
1335
  });
2577
1336
  console.log("getFields response:", result);
@@ -2625,11 +1384,11 @@ tree:
2625
1384
  ])
2626
1385
  );
2627
1386
  expect(mockFetch).toHaveBeenCalledWith(
2628
- `https://api.quickbase.com/v1/fields?tableId=dummyTableId&includeFieldPerms=true`,
1387
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}&includeFieldPerms=true`,
2629
1388
  expect.objectContaining({
2630
1389
  headers: expect.objectContaining({
2631
- Authorization: `QB-USER-TOKEN ${process.env.QB_USER_TOKEN}`,
2632
- "QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
1390
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1391
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2633
1392
  }),
2634
1393
  })
2635
1394
  );
@@ -2649,20 +1408,24 @@ tree:
2649
1408
  type: file
2650
1409
  contents: |
2651
1410
  // tests/vitest/unit/records/deleteRecords.test.ts
2652
- import { describe, expect, test, beforeEach } from "vitest"; // Added 'describe'
2653
- import { createClient, mockFetch } from "@tests/setup.ts";
1411
+ import { describe, expect, test, beforeEach } from "vitest";
1412
+ import {
1413
+ createClient,
1414
+ mockFetch,
1415
+ QB_REALM,
1416
+ QB_USER_TOKEN,
1417
+ QB_TABLE_ID_1,
1418
+ } from "@tests/setup.ts";
1419
+
1420
+ describe("QuickbaseClient Unit - deleteRecords", () => {
1421
+ let qb: ReturnType<typeof createClient>;
2654
1422
 
2655
- describe("QuickbaseClient - deleteRecords (Unit)", () => {
2656
1423
  beforeEach(() => {
2657
- mockFetch.mockClear(); // Clear mock state between tests
1424
+ mockFetch.mockClear();
1425
+ qb = createClient(mockFetch, { debug: true }); // Use debug: true for consistency with other tests
2658
1426
  });
2659
1427
 
2660
- test("deleteRecords - sends correct DELETE request and handles success", async () => {
2661
- const qb = createClient(mockFetch, {
2662
- realm: "test-realm",
2663
- userToken: "test-token",
2664
- });
2665
-
1428
+ test("sends correct DELETE request and handles success", async () => {
2666
1429
  mockFetch.mockResolvedValueOnce({
2667
1430
  ok: true,
2668
1431
  status: 200,
@@ -2671,7 +1434,7 @@ tree:
2671
1434
 
2672
1435
  const result = await qb.deleteRecords({
2673
1436
  body: {
2674
- from: "test-table-id",
1437
+ from: QB_TABLE_ID_1,
2675
1438
  where: "{3.EX.'5'}", // Simulate deleting Record ID 5
2676
1439
  },
2677
1440
  });
@@ -2681,22 +1444,17 @@ tree:
2681
1444
  expect.objectContaining({
2682
1445
  method: "DELETE",
2683
1446
  headers: expect.objectContaining({
2684
- "QB-Realm-Hostname": "test-realm.quickbase.com",
2685
- Authorization: "QB-USER-TOKEN test-token",
1447
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1448
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2686
1449
  "Content-Type": "application/json",
2687
1450
  }),
2688
- body: JSON.stringify({ from: "test-table-id", where: "{3.EX.'5'}" }),
1451
+ body: JSON.stringify({ from: QB_TABLE_ID_1, where: "{3.EX.'5'}" }),
2689
1452
  })
2690
1453
  );
2691
1454
  expect(result).toEqual({ numberDeleted: 1 });
2692
1455
  });
2693
1456
 
2694
- test("deleteRecords - handles no records deleted", async () => {
2695
- const qb = createClient(mockFetch, {
2696
- realm: "test-realm",
2697
- userToken: "test-token",
2698
- });
2699
-
1457
+ test("handles no records deleted", async () => {
2700
1458
  mockFetch.mockResolvedValueOnce({
2701
1459
  ok: true,
2702
1460
  status: 200,
@@ -2705,7 +1463,7 @@ tree:
2705
1463
 
2706
1464
  const result = await qb.deleteRecords({
2707
1465
  body: {
2708
- from: "test-table-id",
1466
+ from: QB_TABLE_ID_1,
2709
1467
  where: "{3.EX.'999999'}", // Non-existent Record ID
2710
1468
  },
2711
1469
  });
@@ -2715,12 +1473,12 @@ tree:
2715
1473
  expect.objectContaining({
2716
1474
  method: "DELETE",
2717
1475
  headers: expect.objectContaining({
2718
- "QB-Realm-Hostname": "test-realm.quickbase.com",
2719
- Authorization: "QB-USER-TOKEN test-token",
1476
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1477
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2720
1478
  "Content-Type": "application/json",
2721
1479
  }),
2722
1480
  body: JSON.stringify({
2723
- from: "test-table-id",
1481
+ from: QB_TABLE_ID_1,
2724
1482
  where: "{3.EX.'999999'}",
2725
1483
  }),
2726
1484
  })
@@ -2728,12 +1486,7 @@ tree:
2728
1486
  expect(result).toEqual({ numberDeleted: 0 });
2729
1487
  });
2730
1488
 
2731
- test("deleteRecords - throws error on API failure", async () => {
2732
- const qb = createClient(mockFetch, {
2733
- realm: "test-realm",
2734
- userToken: "test-token",
2735
- });
2736
-
1489
+ test("throws error on API failure", async () => {
2737
1490
  mockFetch.mockResolvedValueOnce({
2738
1491
  ok: false,
2739
1492
  status: 400,
@@ -2743,25 +1496,130 @@ tree:
2743
1496
  await expect(() =>
2744
1497
  qb.deleteRecords({
2745
1498
  body: {
2746
- from: "test-table-id",
1499
+ from: QB_TABLE_ID_1,
2747
1500
  where: "{3.EX.'invalid'}",
2748
1501
  },
2749
1502
  })
2750
1503
  ).rejects.toThrow("API Error: Invalid query (Status: 400)");
2751
1504
 
2752
1505
  expect(mockFetch).toHaveBeenCalledWith(
2753
- "https://api.quickbase.com/v1/records",
1506
+ "https://api.quickbase.com/v1/records",
1507
+ expect.objectContaining({
1508
+ method: "DELETE",
1509
+ headers: expect.objectContaining({
1510
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1511
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
1512
+ "Content-Type": "application/json",
1513
+ }),
1514
+ body: JSON.stringify({
1515
+ from: QB_TABLE_ID_1,
1516
+ where: "{3.EX.'invalid'}",
1517
+ }),
1518
+ })
1519
+ );
1520
+ });
1521
+ });
1522
+ - name: runQuery.test.ts
1523
+ type: file
1524
+ contents: |
1525
+ // tests/vitest/unit/records/runQuery.test.ts
1526
+ import { describe, test, expect, beforeEach } from "vitest";
1527
+ import {
1528
+ createClient,
1529
+ mockFetch,
1530
+ QB_REALM,
1531
+ QB_USER_TOKEN,
1532
+ QB_TABLE_ID_1,
1533
+ } from "@tests/setup.ts";
1534
+ import { RunQueryRequest, RunQueryResponse } from "@/generated";
1535
+
1536
+ describe("QuickbaseClient Unit - runQuery", () => {
1537
+ let client: ReturnType<typeof createClient>;
1538
+
1539
+ beforeEach(() => {
1540
+ mockFetch.mockClear();
1541
+ client = createClient(mockFetch, { debug: true });
1542
+ });
1543
+
1544
+ test("sends correct POST request and handles successful query response", async () => {
1545
+ const queryRequest: RunQueryRequest = {
1546
+ from: QB_TABLE_ID_1,
1547
+ select: [3, 6, 7],
1548
+ where: "{6.EX.'Task 1'}",
1549
+ sortBy: [{ fieldId: 6, order: "ASC" }],
1550
+ groupBy: [{ fieldId: 7, grouping: "equal-values" }],
1551
+ options: { skip: 0, top: 100, compareWithAppLocalTime: false },
1552
+ };
1553
+
1554
+ const mockResponse: RunQueryResponse = {
1555
+ data: [
1556
+ { "3": { value: 1 }, "6": { value: "Task 1" }, "7": { value: "High" } },
1557
+ ],
1558
+ fields: [
1559
+ { id: 3, label: "Record ID#", type: "recordid" },
1560
+ { id: 6, label: "Task Name", type: "text" },
1561
+ { id: 7, label: "Priority", type: "text" },
1562
+ ],
1563
+ metadata: {
1564
+ numFields: 3,
1565
+ numRecords: 1,
1566
+ skip: 0,
1567
+ top: 100,
1568
+ totalRecords: 1,
1569
+ },
1570
+ };
1571
+
1572
+ mockFetch.mockResolvedValueOnce({
1573
+ ok: true,
1574
+ status: 200,
1575
+ json: () => Promise.resolve(mockResponse),
1576
+ });
1577
+
1578
+ const result = await client.runQuery({ body: queryRequest });
1579
+
1580
+ expect(mockFetch).toHaveBeenCalledWith(
1581
+ "https://api.quickbase.com/v1/records/query",
1582
+ expect.objectContaining({
1583
+ method: "POST",
1584
+ headers: expect.objectContaining({
1585
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1586
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
1587
+ "Content-Type": "application/json",
1588
+ }),
1589
+ body: JSON.stringify(queryRequest),
1590
+ })
1591
+ );
1592
+
1593
+ expect(result).toEqual(mockResponse);
1594
+ });
1595
+
1596
+ test("handles API error with invalid query", async () => {
1597
+ const invalidQueryRequest = {
1598
+ from: QB_TABLE_ID_1,
1599
+ select: [3],
1600
+ where: "{999.EX.'Invalid'}", // Invalid field ID
1601
+ };
1602
+
1603
+ mockFetch.mockResolvedValueOnce({
1604
+ ok: false,
1605
+ status: 400,
1606
+ json: () => Promise.resolve({ message: "Invalid query" }),
1607
+ });
1608
+
1609
+ await expect(
1610
+ client.runQuery({ body: invalidQueryRequest })
1611
+ ).rejects.toThrow("API Error: Invalid query (Status: 400)");
1612
+
1613
+ expect(mockFetch).toHaveBeenCalledWith(
1614
+ "https://api.quickbase.com/v1/records/query",
2754
1615
  expect.objectContaining({
2755
- method: "DELETE",
1616
+ method: "POST",
2756
1617
  headers: expect.objectContaining({
2757
- "QB-Realm-Hostname": "test-realm.quickbase.com",
2758
- Authorization: "QB-USER-TOKEN test-token",
1618
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1619
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2759
1620
  "Content-Type": "application/json",
2760
1621
  }),
2761
- body: JSON.stringify({
2762
- from: "test-table-id",
2763
- where: "{3.EX.'invalid'}",
2764
- }),
1622
+ body: JSON.stringify(invalidQueryRequest),
2765
1623
  })
2766
1624
  );
2767
1625
  });
@@ -2771,22 +1629,24 @@ tree:
2771
1629
  contents: |
2772
1630
  // tests/vitest/unit/records/upsert.test.ts
2773
1631
  import { describe, expect, test, beforeEach } from "vitest";
2774
- import { createClient, mockFetch } from "@tests/setup.ts";
1632
+ import {
1633
+ createClient,
1634
+ mockFetch,
1635
+ QB_REALM,
1636
+ QB_USER_TOKEN,
1637
+ QB_TABLE_ID_1,
1638
+ } from "@tests/setup.ts";
2775
1639
  import { vi } from "vitest";
2776
1640
 
2777
1641
  // Note: Type augmentation moved to src/types.d.ts
2778
- describe("QuickbaseClient - upsert (Unit)", () => {
1642
+ describe("QuickbaseClient Unit - upsert", () => {
2779
1643
  beforeEach(() => {
2780
1644
  vi.resetModules(); // Reset module cache
2781
1645
  mockFetch.mockClear();
2782
1646
  });
2783
1647
 
2784
1648
  test("sends correct POST request and handles successful upsert", async () => {
2785
- const qb = createClient(mockFetch, {
2786
- realm: "test-realm",
2787
- userToken: "test-token",
2788
- debug: true,
2789
- });
1649
+ const qb = createClient(mockFetch, { debug: true });
2790
1650
 
2791
1651
  mockFetch.mockResolvedValueOnce({
2792
1652
  ok: true,
@@ -2804,10 +1664,10 @@ tree:
2804
1664
  });
2805
1665
 
2806
1666
  console.log("[Test] Calling qb.upsert with:", {
2807
- body: { to: "test-table-id", data: [{ "6": { value: "value1" } }] },
1667
+ body: { to: QB_TABLE_ID_1, data: [{ "6": { value: "value1" } }] },
2808
1668
  });
2809
1669
  const result = await qb.upsert({
2810
- body: { to: "test-table-id", data: [{ "6": { value: "value1" } }] },
1670
+ body: { to: QB_TABLE_ID_1, data: [{ "6": { value: "value1" } }] },
2811
1671
  });
2812
1672
 
2813
1673
  console.log("[Test] mockFetch calls:", mockFetch.mock.calls);
@@ -2816,12 +1676,12 @@ tree:
2816
1676
  expect.objectContaining({
2817
1677
  method: "POST",
2818
1678
  headers: expect.objectContaining({
2819
- "QB-Realm-Hostname": "test-realm.quickbase.com",
2820
- Authorization: "QB-USER-TOKEN test-token",
1679
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1680
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2821
1681
  "Content-Type": "application/json",
2822
1682
  }),
2823
1683
  body: JSON.stringify({
2824
- to: "test-table-id",
1684
+ to: QB_TABLE_ID_1,
2825
1685
  data: [{ "6": { value: "value1" } }],
2826
1686
  }),
2827
1687
  })
@@ -2839,11 +1699,7 @@ tree:
2839
1699
  });
2840
1700
 
2841
1701
  test("handles API error with invalid data", async () => {
2842
- const qb = createClient(mockFetch, {
2843
- realm: "test-realm",
2844
- userToken: "test-token",
2845
- debug: true,
2846
- });
1702
+ const qb = createClient(mockFetch, { debug: true });
2847
1703
 
2848
1704
  mockFetch.mockResolvedValueOnce({
2849
1705
  ok: false,
@@ -2853,7 +1709,7 @@ tree:
2853
1709
 
2854
1710
  await expect(
2855
1711
  qb.upsert({
2856
- body: { to: "test-table-id", data: [{ "999": { value: "invalid" } }] },
1712
+ body: { to: QB_TABLE_ID_1, data: [{ "999": { value: "invalid" } }] },
2857
1713
  })
2858
1714
  ).rejects.toThrow("API Error: Invalid data format (Status: 400)");
2859
1715
 
@@ -2862,12 +1718,12 @@ tree:
2862
1718
  expect.objectContaining({
2863
1719
  method: "POST",
2864
1720
  headers: expect.objectContaining({
2865
- "QB-Realm-Hostname": "test-realm.quickbase.com",
2866
- Authorization: "QB-USER-TOKEN test-token",
1721
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1722
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2867
1723
  "Content-Type": "application/json",
2868
1724
  }),
2869
1725
  body: JSON.stringify({
2870
- to: "test-table-id",
1726
+ to: QB_TABLE_ID_1,
2871
1727
  data: [{ "999": { value: "invalid" } }],
2872
1728
  }),
2873
1729
  })
@@ -2875,11 +1731,7 @@ tree:
2875
1731
  });
2876
1732
 
2877
1733
  test("handles upsert with temp token", async () => {
2878
- const qb = createClient(mockFetch, {
2879
- realm: "test-realm",
2880
- useTempTokens: true,
2881
- debug: true,
2882
- });
1734
+ const qb = createClient(mockFetch, { useTempTokens: true, debug: true });
2883
1735
 
2884
1736
  mockFetch
2885
1737
  .mockResolvedValueOnce({
@@ -2903,18 +1755,18 @@ tree:
2903
1755
  });
2904
1756
 
2905
1757
  const result = await qb.upsert({
2906
- body: { to: "test-table-id", data: [{ "7": { value: "value2" } }] },
2907
- dbid: "test-dbid",
1758
+ body: { to: QB_TABLE_ID_1, data: [{ "7": { value: "value2" } }] },
1759
+ dbid: QB_TABLE_ID_1, // Use QB_TABLE_ID_1 as dbid for temp token
2908
1760
  } as any); // Temporary type assertion until types.d.ts is applied
2909
1761
 
2910
1762
  expect(mockFetch).toHaveBeenCalledTimes(2);
2911
1763
  expect(mockFetch).toHaveBeenNthCalledWith(
2912
1764
  1,
2913
- "https://api.quickbase.com/v1/auth/temporary/test-dbid",
1765
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
2914
1766
  expect.objectContaining({
2915
1767
  method: "GET",
2916
1768
  headers: expect.objectContaining({
2917
- "QB-Realm-Hostname": "test-realm.quickbase.com",
1769
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2918
1770
  "Content-Type": "application/json",
2919
1771
  }),
2920
1772
  credentials: "include",
@@ -2926,12 +1778,12 @@ tree:
2926
1778
  expect.objectContaining({
2927
1779
  method: "POST",
2928
1780
  headers: expect.objectContaining({
2929
- "QB-Realm-Hostname": "test-realm.quickbase.com",
1781
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2930
1782
  Authorization: "QB-TEMP-TOKEN temp-token",
2931
1783
  "Content-Type": "application/json",
2932
1784
  }),
2933
1785
  body: JSON.stringify({
2934
- to: "test-table-id",
1786
+ to: QB_TABLE_ID_1,
2935
1787
  data: [{ "7": { value: "value2" } }],
2936
1788
  }),
2937
1789
  })
@@ -2949,11 +1801,7 @@ tree:
2949
1801
  });
2950
1802
 
2951
1803
  test("sends correct POST request with data fields", async () => {
2952
- const qb = createClient(mockFetch, {
2953
- realm: "test-realm",
2954
- userToken: "test-token",
2955
- debug: true,
2956
- });
1804
+ const qb = createClient(mockFetch, { debug: true });
2957
1805
 
2958
1806
  mockFetch.mockResolvedValueOnce({
2959
1807
  ok: true,
@@ -2971,7 +1819,7 @@ tree:
2971
1819
  });
2972
1820
 
2973
1821
  const result = await qb.upsert({
2974
- body: { to: "test-table-id", data: [{ "6": { value: "value1" } }] },
1822
+ body: { to: QB_TABLE_ID_1, data: [{ "6": { value: "value1" } }] },
2975
1823
  });
2976
1824
 
2977
1825
  expect(mockFetch).toHaveBeenCalledWith(
@@ -2979,12 +1827,12 @@ tree:
2979
1827
  expect.objectContaining({
2980
1828
  method: "POST",
2981
1829
  headers: expect.objectContaining({
2982
- "QB-Realm-Hostname": "test-realm.quickbase.com",
2983
- Authorization: "QB-USER-TOKEN test-token",
1830
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1831
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2984
1832
  "Content-Type": "application/json",
2985
1833
  }),
2986
1834
  body: JSON.stringify({
2987
- to: "test-table-id",
1835
+ to: QB_TABLE_ID_1,
2988
1836
  data: [{ "6": { value: "value1" } }],
2989
1837
  }),
2990
1838
  })
@@ -3003,10 +1851,17 @@ tree:
3003
1851
  - name: createTable.test.ts
3004
1852
  type: file
3005
1853
  contents: |
3006
- import { describe, it, expect, beforeEach, vi } from "vitest";
3007
- import { createClient, mockFetch } from "@tests/setup.ts";
3008
-
3009
- describe("QuickbaseClient - createTable (Unit)", () => {
1854
+ // tests/vitest/unit/records/createTable.test.ts
1855
+ import { describe, it, expect, beforeEach } from "vitest";
1856
+ import {
1857
+ createClient,
1858
+ mockFetch,
1859
+ QB_APP_ID,
1860
+ QB_REALM,
1861
+ QB_USER_TOKEN,
1862
+ } from "@tests/setup.ts";
1863
+
1864
+ describe("QuickbaseClient Unit - createTable", () => {
3010
1865
  let client: ReturnType<typeof createClient>;
3011
1866
 
3012
1867
  beforeEach(() => {
@@ -3015,7 +1870,6 @@ tree:
3015
1870
  });
3016
1871
 
3017
1872
  it("calls createTable successfully with user token", async () => {
3018
- const appId = "buwai2zpe";
3019
1873
  const mockBody = {
3020
1874
  name: "TestTable",
3021
1875
  description: "Unit test table",
@@ -3023,7 +1877,7 @@ tree:
3023
1877
  pluralRecordName: "Tests",
3024
1878
  };
3025
1879
  const mockResponse = {
3026
- id: "buya8h9iz",
1880
+ id: "newly-created-table-id-1234567890", // Changed from "buya8h9iz"
3027
1881
  name: "TestTable",
3028
1882
  alias: "_DBID_TESTTABLE",
3029
1883
  description: "Unit test table",
@@ -3047,18 +1901,20 @@ tree:
3047
1901
  json: () => Promise.resolve(mockResponse),
3048
1902
  });
3049
1903
 
3050
- const response = await client.createTable({ appId, body: mockBody });
1904
+ const response = await client.createTable({
1905
+ appId: QB_APP_ID,
1906
+ body: mockBody,
1907
+ });
3051
1908
  expect(response).toEqual(mockResponse);
3052
1909
  expect(mockFetch).toHaveBeenCalledTimes(1);
3053
1910
  expect(mockFetch).toHaveBeenCalledWith(
3054
- `https://api.quickbase.com/v1/tables?appId=${appId}`,
1911
+ `https://api.quickbase.com/v1/tables?appId=${QB_APP_ID}`,
3055
1912
  expect.objectContaining({
3056
1913
  method: "POST",
3057
1914
  headers: expect.objectContaining({
3058
1915
  "Content-Type": "application/json",
3059
- "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
3060
- Authorization:
3061
- "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
1916
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1917
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
3062
1918
  }),
3063
1919
  body: JSON.stringify(mockBody),
3064
1920
  })
@@ -3068,10 +1924,18 @@ tree:
3068
1924
  - name: deleteTable.test.ts
3069
1925
  type: file
3070
1926
  contents: |
3071
- import { describe, it, expect, beforeEach, vi } from "vitest";
3072
- import { createClient, mockFetch } from "@tests/setup.ts";
3073
-
3074
- describe("QuickbaseClient - deleteTable (Unit)", () => {
1927
+ // tests/vitest/unit/records/deleteTable.test.ts
1928
+ import { describe, it, expect, beforeEach } from "vitest";
1929
+ import {
1930
+ createClient,
1931
+ mockFetch,
1932
+ QB_APP_ID,
1933
+ QB_TABLE_ID_1,
1934
+ QB_REALM,
1935
+ QB_USER_TOKEN,
1936
+ } from "@tests/setup.ts";
1937
+
1938
+ describe("QuickbaseClient Unit - deleteTable", () => {
3075
1939
  let client: ReturnType<typeof createClient>;
3076
1940
 
3077
1941
  beforeEach(() => {
@@ -3080,10 +1944,8 @@ tree:
3080
1944
  });
3081
1945
 
3082
1946
  it("calls deleteTable successfully with user token", async () => {
3083
- const appId = "buwai2zpe";
3084
- const tableId = "buya8h9iz";
3085
1947
  const mockResponse = {
3086
- deletedTableId: tableId,
1948
+ deletedTableId: QB_TABLE_ID_1,
3087
1949
  };
3088
1950
 
3089
1951
  mockFetch.mockResolvedValueOnce({
@@ -3092,27 +1954,26 @@ tree:
3092
1954
  json: () => Promise.resolve(mockResponse),
3093
1955
  });
3094
1956
 
3095
- const response = await client.deleteTable({ tableId, appId });
1957
+ const response = await client.deleteTable({
1958
+ tableId: QB_TABLE_ID_1,
1959
+ appId: QB_APP_ID,
1960
+ });
3096
1961
  expect(response).toEqual(mockResponse);
3097
1962
  expect(mockFetch).toHaveBeenCalledTimes(1);
3098
1963
  expect(mockFetch).toHaveBeenCalledWith(
3099
- `https://api.quickbase.com/v1/tables/${tableId}?appId=${appId}`,
1964
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
3100
1965
  expect.objectContaining({
3101
1966
  method: "DELETE",
3102
1967
  headers: expect.objectContaining({
3103
1968
  "Content-Type": "application/json",
3104
- "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
3105
- Authorization:
3106
- "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
1969
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1970
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
3107
1971
  }),
3108
1972
  })
3109
1973
  );
3110
1974
  });
3111
1975
 
3112
1976
  it("handles 404 error for non-existent table", async () => {
3113
- const appId = "buwai2zpe";
3114
- const tableId = "nonexistent";
3115
-
3116
1977
  mockFetch.mockResolvedValueOnce({
3117
1978
  ok: false,
3118
1979
  status: 404,
@@ -3123,15 +1984,19 @@ tree:
3123
1984
  }),
3124
1985
  });
3125
1986
 
3126
- await expect(client.deleteTable({ tableId, appId })).rejects.toThrow(
3127
- "API Error: Invalid DBID (Status: 404)"
3128
- );
1987
+ await expect(
1988
+ client.deleteTable({ tableId: QB_TABLE_ID_1, appId: QB_APP_ID })
1989
+ ).rejects.toThrow("API Error: Invalid DBID (Status: 404)");
3129
1990
  expect(mockFetch).toHaveBeenCalledTimes(1);
3130
1991
  expect(mockFetch).toHaveBeenCalledWith(
3131
- `https://api.quickbase.com/v1/tables/${tableId}?appId=${appId}`,
1992
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
3132
1993
  expect.objectContaining({
3133
1994
  method: "DELETE",
3134
- headers: expect.any(Object),
1995
+ headers: expect.objectContaining({
1996
+ "Content-Type": "application/json",
1997
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
1998
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
1999
+ }),
3135
2000
  })
3136
2001
  );
3137
2002
  });
@@ -3139,10 +2004,24 @@ tree:
3139
2004
  - name: getAppTables.test.ts
3140
2005
  type: file
3141
2006
  contents: |
3142
- import { describe, it, expect } from "vitest";
3143
- import { createClient, mockFetch } from "@tests/setup.ts";
2007
+ // tests/vitest/unit/tables/getAppTables.test.ts
2008
+ import { describe, it, expect, beforeEach } from "vitest";
2009
+ import {
2010
+ createClient,
2011
+ mockFetch,
2012
+ QB_APP_ID,
2013
+ QB_REALM,
2014
+ QB_USER_TOKEN,
2015
+ } from "@tests/setup.ts";
2016
+
2017
+ describe("QuickbaseClient Unit - getAppTables", () => {
2018
+ let client: ReturnType<typeof createClient>;
2019
+
2020
+ beforeEach(() => {
2021
+ mockFetch.mockClear();
2022
+ client = createClient(mockFetch, { debug: true }); // Add debug: true for consistency
2023
+ });
3144
2024
 
3145
- describe("QuickbaseClient - getAppTables (Unit)", () => {
3146
2025
  it("calls getAppTables successfully", async () => {
3147
2026
  mockFetch.mockImplementation((url: string, options: any) => {
3148
2027
  console.log("Mock fetch for getAppTables:", url, options);
@@ -3152,84 +2031,352 @@ tree:
3152
2031
  json: () =>
3153
2032
  Promise.resolve([
3154
2033
  {
2034
+ id: "buwai2zr4",
2035
+ name: "Root",
3155
2036
  alias: "_DBID_ROOT",
3156
- created: "2025-02-13T18:22:33Z",
2037
+ description: "",
2038
+ created: new Date("2025-02-13T18:22:33.000Z"),
2039
+ updated: new Date("2025-02-13T18:22:34.000Z"),
2040
+ nextRecordId: 1,
2041
+ nextFieldId: 6,
3157
2042
  defaultSortFieldId: 2,
3158
2043
  defaultSortOrder: "DESC",
3159
- description: "",
3160
- id: "buwai2zr4",
3161
2044
  keyFieldId: 3,
3162
- name: "Root",
3163
- nextFieldId: 6,
3164
- nextRecordId: 1,
3165
- pluralRecordName: "Roots",
3166
2045
  singleRecordName: "Root",
2046
+ pluralRecordName: "Roots",
3167
2047
  sizeLimit: "500 MB",
3168
- spaceRemaining: "500 MB",
3169
2048
  spaceUsed: "0 KB",
3170
- updated: "2025-02-13T18:22:34Z",
2049
+ spaceRemaining: "500 MB",
3171
2050
  },
3172
2051
  ]),
3173
2052
  } as Response);
3174
2053
  });
3175
2054
 
3176
- const client = createClient(mockFetch);
3177
- const tablesAppId = process.env.QB_APP_ID;
3178
- if (!tablesAppId) throw new Error("QB_APP_ID is not defined in .env");
3179
- const result = await client.getAppTables({ appId: tablesAppId });
2055
+ const result = await client.getAppTables({ appId: QB_APP_ID });
3180
2056
  console.log("getAppTables response:", result);
3181
2057
  expect(result).toEqual(
3182
2058
  expect.arrayContaining([
3183
2059
  expect.objectContaining({
2060
+ id: "buwai2zr4",
2061
+ name: "Root",
3184
2062
  alias: "_DBID_ROOT",
3185
- created: new Date("2025-02-13T18:22:33Z"),
2063
+ description: "",
2064
+ created: new Date("2025-02-13T18:22:33.000Z"),
2065
+ updated: new Date("2025-02-13T18:22:34.000Z"),
2066
+ nextRecordId: 1,
2067
+ nextFieldId: 6,
3186
2068
  defaultSortFieldId: 2,
3187
2069
  defaultSortOrder: "DESC",
3188
- description: "",
3189
- id: "buwai2zr4",
3190
2070
  keyFieldId: 3,
3191
- name: "Root",
3192
- nextFieldId: 6,
3193
- nextRecordId: 1,
3194
- pluralRecordName: "Roots",
3195
2071
  singleRecordName: "Root",
2072
+ pluralRecordName: "Roots",
3196
2073
  sizeLimit: "500 MB",
3197
- spaceRemaining: "500 MB",
3198
2074
  spaceUsed: "0 KB",
3199
- updated: new Date("2025-02-13T18:22:34Z"),
2075
+ spaceRemaining: "500 MB",
3200
2076
  }),
3201
2077
  ])
3202
2078
  );
3203
2079
  expect(mockFetch).toHaveBeenCalledWith(
3204
- `https://api.quickbase.com/v1/tables?appId=${tablesAppId}`,
2080
+ `https://api.quickbase.com/v1/tables?appId=${QB_APP_ID}`,
3205
2081
  expect.objectContaining({
2082
+ method: "GET",
3206
2083
  headers: expect.objectContaining({
3207
- Authorization: `QB-USER-TOKEN ${process.env.QB_USER_TOKEN}`,
3208
- "QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
2084
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2085
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2086
+ "Content-Type": "application/json",
3209
2087
  }),
3210
2088
  })
3211
2089
  );
3212
2090
  });
3213
2091
  });
3214
- - name: getTable.test.ts
2092
+ - name: getRelationships.test.ts
3215
2093
  type: file
3216
2094
  contents: |
3217
- import { describe, it, expect, beforeEach, vi } from "vitest";
3218
- import { createClient, mockFetch } from "@tests/setup.ts";
2095
+ // tests/vitest/unit/relationships/getRelationships.test.ts
2096
+ import { describe, it, expect, beforeEach } from "vitest";
2097
+ import {
2098
+ createClient,
2099
+ mockFetch,
2100
+ QB_REALM,
2101
+ QB_USER_TOKEN,
2102
+ QB_TABLE_ID_1,
2103
+ QB_TABLE_ID_2,
2104
+ } from "@tests/setup.ts";
2105
+ import { GetRelationships200Response } from "/home/drew/Projects/quickbase-js/src/generated/models";
2106
+
2107
+ describe("QuickbaseClient Unit - getRelationships", () => {
2108
+ let client: ReturnType<typeof createClient>;
2109
+
2110
+ beforeEach(() => {
2111
+ mockFetch.mockClear();
2112
+ client = createClient(mockFetch, { debug: true }); // Consistent with output
2113
+ });
2114
+
2115
+ it("initializes without errors", () => {
2116
+ expect(client).toBeDefined();
2117
+ });
2118
+
2119
+ it("has getRelationships method", () => {
2120
+ expect(typeof client.getRelationships).toBe("function");
2121
+ });
2122
+
2123
+ it("sends correct GET request and handles successful response", async () => {
2124
+ const mockResponse: GetRelationships200Response = {
2125
+ metadata: {
2126
+ numRelationships: 1,
2127
+ skip: 0,
2128
+ totalRelationships: 1,
2129
+ },
2130
+ relationships: [
2131
+ {
2132
+ id: 6,
2133
+ parentTableId: QB_TABLE_ID_1, // Parent table
2134
+ childTableId: QB_TABLE_ID_2, // Child table
2135
+ foreignKeyField: {
2136
+ id: 6,
2137
+ label: "Related record",
2138
+ type: "numeric",
2139
+ },
2140
+ isCrossApp: false,
2141
+ lookupFields: [
2142
+ {
2143
+ id: 3,
2144
+ label: "Name",
2145
+ type: "text",
2146
+ },
2147
+ ],
2148
+ summaryFields: [
2149
+ {
2150
+ id: 8,
2151
+ label: "my summary field",
2152
+ type: "numeric",
2153
+ },
2154
+ ],
2155
+ },
2156
+ ],
2157
+ };
2158
+
2159
+ mockFetch.mockResolvedValueOnce({
2160
+ ok: true,
2161
+ status: 200,
2162
+ json: () => Promise.resolve(mockResponse),
2163
+ });
2164
+
2165
+ const result = await client.getRelationships({
2166
+ tableId: QB_TABLE_ID_2,
2167
+ skip: 0,
2168
+ });
2169
+
2170
+ expect(mockFetch).toHaveBeenCalledWith(
2171
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationships?skip=0`,
2172
+ expect.objectContaining({
2173
+ method: "GET",
2174
+ headers: expect.objectContaining({
2175
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2176
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2177
+ "Content-Type": "application/json",
2178
+ }),
2179
+ })
2180
+ );
2181
+
2182
+ expect(result).toEqual(mockResponse);
2183
+ expect(result.metadata.numRelationships).toBe(1);
2184
+ expect(result.relationships).toHaveLength(1);
2185
+ expect(result.relationships[0].id).toBe(6);
2186
+ expect(result.relationships[0].foreignKeyField.label).toBe(
2187
+ "Related record"
2188
+ );
2189
+ });
2190
+
2191
+ it("handles request without skip parameter", async () => {
2192
+ const mockResponse: GetRelationships200Response = {
2193
+ metadata: {
2194
+ numRelationships: 1,
2195
+ skip: 0,
2196
+ totalRelationships: 1,
2197
+ },
2198
+ relationships: [
2199
+ {
2200
+ id: 6,
2201
+ parentTableId: QB_TABLE_ID_1,
2202
+ childTableId: QB_TABLE_ID_2,
2203
+ foreignKeyField: {
2204
+ id: 6,
2205
+ label: "Related record",
2206
+ type: "numeric",
2207
+ },
2208
+ isCrossApp: false,
2209
+ lookupFields: [],
2210
+ summaryFields: [],
2211
+ },
2212
+ ],
2213
+ };
3219
2214
 
3220
- describe("QuickbaseClient - getTable (Unit)", () => {
2215
+ mockFetch.mockResolvedValueOnce({
2216
+ ok: true,
2217
+ status: 200,
2218
+ json: () => Promise.resolve(mockResponse),
2219
+ });
2220
+
2221
+ const result = await client.getRelationships({
2222
+ tableId: QB_TABLE_ID_2, // Corrected from QB_TABLE_ID_1
2223
+ });
2224
+
2225
+ expect(mockFetch).toHaveBeenCalledWith(
2226
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationships`,
2227
+ expect.objectContaining({
2228
+ method: "GET",
2229
+ headers: expect.objectContaining({
2230
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2231
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2232
+ "Content-Type": "application/json",
2233
+ }),
2234
+ })
2235
+ );
2236
+
2237
+ expect(result).toEqual(mockResponse);
2238
+ });
2239
+
2240
+ it("handles API error response", async () => {
2241
+ mockFetch.mockResolvedValueOnce({
2242
+ ok: false,
2243
+ status: 404,
2244
+ json: () => Promise.resolve({ message: "Table not found" }),
2245
+ });
2246
+
2247
+ await expect(
2248
+ client.getRelationships({
2249
+ tableId: QB_TABLE_ID_2, // Corrected from QB_TABLE_ID_1
2250
+ skip: 0,
2251
+ })
2252
+ ).rejects.toThrow("API Error: Table not found (Status: 404)");
2253
+
2254
+ expect(mockFetch).toHaveBeenCalledWith(
2255
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationships?skip=0`,
2256
+ expect.objectContaining({
2257
+ method: "GET",
2258
+ headers: expect.objectContaining({
2259
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2260
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2261
+ "Content-Type": "application/json",
2262
+ }),
2263
+ })
2264
+ );
2265
+ });
2266
+
2267
+ it("retries with temp token after 401", async () => {
2268
+ const mockResponse: GetRelationships200Response = {
2269
+ metadata: {
2270
+ numRelationships: 1,
2271
+ skip: 0,
2272
+ totalRelationships: 1,
2273
+ },
2274
+ relationships: [
2275
+ {
2276
+ id: 6,
2277
+ parentTableId: QB_TABLE_ID_1,
2278
+ childTableId: QB_TABLE_ID_2,
2279
+ foreignKeyField: {
2280
+ id: 6,
2281
+ label: "Related record",
2282
+ type: "numeric",
2283
+ },
2284
+ isCrossApp: false,
2285
+ },
2286
+ ],
2287
+ };
2288
+
2289
+ mockFetch
2290
+ .mockResolvedValueOnce({
2291
+ ok: true,
2292
+ status: 200,
2293
+ json: () =>
2294
+ Promise.resolve({ temporaryAuthorization: "initial_token" }),
2295
+ })
2296
+ .mockResolvedValueOnce({
2297
+ ok: false,
2298
+ status: 401,
2299
+ json: () => Promise.resolve({ message: "Unauthorized" }),
2300
+ })
2301
+ .mockResolvedValueOnce({
2302
+ ok: true,
2303
+ status: 200,
2304
+ json: () => Promise.resolve({ temporaryAuthorization: "new_token" }),
2305
+ })
2306
+ .mockResolvedValueOnce({
2307
+ ok: true,
2308
+ status: 200,
2309
+ json: () => Promise.resolve(mockResponse),
2310
+ });
2311
+
2312
+ const tempClient = createClient(mockFetch, {
2313
+ useTempTokens: true,
2314
+ debug: true,
2315
+ });
2316
+ const result = await tempClient.getRelationships({
2317
+ tableId: QB_TABLE_ID_2,
2318
+ skip: 0,
2319
+ });
2320
+
2321
+ expect(mockFetch).toHaveBeenCalledTimes(4);
2322
+ expect(mockFetch).toHaveBeenNthCalledWith(
2323
+ 1,
2324
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_2}`,
2325
+ expect.any(Object)
2326
+ );
2327
+ expect(mockFetch).toHaveBeenNthCalledWith(
2328
+ 2,
2329
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationships?skip=0`,
2330
+ expect.objectContaining({
2331
+ headers: expect.objectContaining({
2332
+ Authorization: "QB-TEMP-TOKEN initial_token",
2333
+ }),
2334
+ })
2335
+ );
2336
+ expect(mockFetch).toHaveBeenNthCalledWith(
2337
+ 3,
2338
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_2}`,
2339
+ expect.any(Object)
2340
+ );
2341
+ expect(mockFetch).toHaveBeenNthCalledWith(
2342
+ 4,
2343
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationships?skip=0`,
2344
+ expect.objectContaining({
2345
+ headers: expect.objectContaining({
2346
+ Authorization: "QB-TEMP-TOKEN new_token",
2347
+ }),
2348
+ })
2349
+ );
2350
+
2351
+ expect(result).toEqual(mockResponse);
2352
+ });
2353
+ });
2354
+ - name: getTable.test.ts
2355
+ type: file
2356
+ contents: |
2357
+ // tests/vitest/unit/tables/getTable.test.ts
2358
+ import { describe, it, expect, beforeEach } from "vitest";
2359
+ import {
2360
+ createClient,
2361
+ mockFetch,
2362
+ QB_APP_ID,
2363
+ QB_TABLE_ID_1,
2364
+ QB_REALM,
2365
+ QB_USER_TOKEN,
2366
+ } from "@tests/setup.ts";
2367
+
2368
+ describe("QuickbaseClient Unit - getTable", () => {
3221
2369
  let client: ReturnType<typeof createClient>;
3222
2370
 
3223
2371
  beforeEach(() => {
3224
2372
  mockFetch.mockClear();
3225
- client = createClient(mockFetch, { debug: true });
3226
2373
  });
3227
2374
 
3228
2375
  it("calls getTable successfully with user token", async () => {
3229
- const mockAppId = "buwai2zpe";
3230
- const mockTableId = "buwai2zr4";
2376
+ client = createClient(mockFetch, { debug: true });
2377
+
3231
2378
  const mockResponse = {
3232
- id: mockTableId,
2379
+ id: QB_TABLE_ID_1,
3233
2380
  name: "Root",
3234
2381
  alias: "_DBID_ROOT",
3235
2382
  description: "",
@@ -3254,17 +2401,17 @@ tree:
3254
2401
  });
3255
2402
 
3256
2403
  const response = await client.getTable({
3257
- tableId: mockTableId,
3258
- appId: mockAppId,
2404
+ tableId: QB_TABLE_ID_1,
2405
+ appId: QB_APP_ID,
3259
2406
  });
3260
2407
  expect(response).toEqual(mockResponse);
3261
2408
  expect(mockFetch).toHaveBeenCalledWith(
3262
- `https://api.quickbase.com/v1/tables/${mockTableId}?appId=${mockAppId}`,
2409
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
3263
2410
  expect.objectContaining({
3264
2411
  method: "GET",
3265
2412
  headers: expect.objectContaining({
3266
- Authorization:
3267
- "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
2413
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2414
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
3268
2415
  }),
3269
2416
  })
3270
2417
  );
@@ -3272,10 +2419,9 @@ tree:
3272
2419
 
3273
2420
  it("calls getTable successfully with temp token", async () => {
3274
2421
  client = createClient(mockFetch, { debug: true, useTempTokens: true });
3275
- const mockAppId = "buwai2zpe";
3276
- const mockTableId = "buwai2zr4";
2422
+
3277
2423
  const mockResponse = {
3278
- id: mockTableId,
2424
+ id: QB_TABLE_ID_1,
3279
2425
  name: "Root",
3280
2426
  alias: "_DBID_ROOT",
3281
2427
  description: "",
@@ -3306,19 +2452,20 @@ tree:
3306
2452
  });
3307
2453
 
3308
2454
  const response = await client.getTable({
3309
- tableId: mockTableId,
3310
- appId: mockAppId,
2455
+ tableId: QB_TABLE_ID_1,
2456
+ appId: QB_APP_ID,
3311
2457
  });
3312
2458
  expect(response).toEqual(mockResponse);
3313
2459
  expect(mockFetch).toHaveBeenCalledTimes(2);
3314
2460
  expect(mockFetch).toHaveBeenCalledWith(
3315
- `https://api.quickbase.com/v1/auth/temporary/${mockTableId}`,
2461
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
3316
2462
  expect.any(Object)
3317
2463
  );
3318
2464
  expect(mockFetch).toHaveBeenCalledWith(
3319
- `https://api.quickbase.com/v1/tables/${mockTableId}?appId=${mockAppId}`,
2465
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
3320
2466
  expect.objectContaining({
3321
2467
  headers: expect.objectContaining({
2468
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
3322
2469
  Authorization: "QB-TEMP-TOKEN temp_token",
3323
2470
  }),
3324
2471
  })
@@ -3327,10 +2474,9 @@ tree:
3327
2474
 
3328
2475
  it("retries successfully after 401 with temp token", async () => {
3329
2476
  client = createClient(mockFetch, { debug: true, useTempTokens: true });
3330
- const mockAppId = "buwai2zpe";
3331
- const mockTableId = "buwai2zr4";
2477
+
3332
2478
  const mockResponse = {
3333
- id: mockTableId,
2479
+ id: QB_TABLE_ID_1,
3334
2480
  name: "Root",
3335
2481
  alias: "_DBID_ROOT",
3336
2482
  description: "",
@@ -3372,17 +2518,23 @@ tree:
3372
2518
  });
3373
2519
 
3374
2520
  const response = await client.getTable({
3375
- tableId: mockTableId,
3376
- appId: mockAppId,
2521
+ tableId: QB_TABLE_ID_1,
2522
+ appId: QB_APP_ID,
3377
2523
  });
3378
2524
  expect(response).toEqual(mockResponse);
3379
- expect(mockFetch).toHaveBeenCalledTimes(4); // Initial token, 401, retry token, success
2525
+ expect(mockFetch).toHaveBeenCalledTimes(4);
2526
+ expect(mockFetch).toHaveBeenCalledWith(
2527
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
2528
+ expect.objectContaining({
2529
+ headers: expect.objectContaining({
2530
+ Authorization: "QB-TEMP-TOKEN new_token",
2531
+ }),
2532
+ })
2533
+ );
3380
2534
  });
3381
2535
 
3382
2536
  it("handles 401 with failed temp token retry", async () => {
3383
2537
  client = createClient(mockFetch, { debug: true, useTempTokens: true });
3384
- const mockAppId = "buwai2zpe";
3385
- const mockTableId = "buwai2zr4";
3386
2538
 
3387
2539
  mockFetch
3388
2540
  .mockResolvedValueOnce({
@@ -3404,7 +2556,7 @@ tree:
3404
2556
  });
3405
2557
 
3406
2558
  await expect(
3407
- client.getTable({ tableId: mockTableId, appId: mockAppId })
2559
+ client.getTable({ tableId: QB_TABLE_ID_1, appId: QB_APP_ID })
3408
2560
  ).rejects.toThrow(
3409
2561
  "API Error: Unauthorized in fetchTempToken (Status: 401)"
3410
2562
  );
@@ -3412,8 +2564,7 @@ tree:
3412
2564
  });
3413
2565
 
3414
2566
  it("handles 404 Not Found", async () => {
3415
- const mockAppId = "buwai2zpe";
3416
- const mockTableId = "invalidTableId";
2567
+ client = createClient(mockFetch, { debug: true });
3417
2568
 
3418
2569
  mockFetch.mockResolvedValueOnce({
3419
2570
  ok: false,
@@ -3422,17 +2573,33 @@ tree:
3422
2573
  });
3423
2574
 
3424
2575
  await expect(
3425
- client.getTable({ tableId: mockTableId, appId: mockAppId })
2576
+ client.getTable({ tableId: QB_TABLE_ID_1, appId: QB_APP_ID })
3426
2577
  ).rejects.toThrow("API Error: Table not found (Status: 404)");
2578
+ expect(mockFetch).toHaveBeenCalledWith(
2579
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
2580
+ expect.objectContaining({
2581
+ method: "GET",
2582
+ headers: expect.objectContaining({
2583
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
2584
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
2585
+ }),
2586
+ })
2587
+ );
3427
2588
  });
3428
2589
  });
3429
2590
  - name: updateTable.test.ts
3430
2591
  type: file
3431
2592
  contents: |
3432
- import { describe, it, expect, beforeEach, vi } from "vitest";
3433
- import { createClient, mockFetch } from "@tests/setup.ts";
3434
-
3435
- describe("QuickbaseClient - updateTable (Unit)", () => {
2593
+ // tests/vitest/unit/records/updateTable.test.ts
2594
+ import { describe, it, expect, beforeEach } from "vitest";
2595
+ import {
2596
+ createClient,
2597
+ mockFetch,
2598
+ QB_APP_ID,
2599
+ QB_TABLE_ID_1,
2600
+ } from "@tests/setup.ts";
2601
+
2602
+ describe("QuickbaseClient Unit - updateTable", () => {
3436
2603
  let client: ReturnType<typeof createClient>;
3437
2604
 
3438
2605
  beforeEach(() => {
@@ -3442,14 +2609,13 @@ tree:
3442
2609
 
3443
2610
  it("calls updateTable successfully with temp token", async () => {
3444
2611
  client = createClient(mockFetch, { debug: true, useTempTokens: true });
3445
- const mockAppId = "buwai2zpe";
3446
- const mockTableId = "buwai2zr4";
2612
+
3447
2613
  const mockBody = {
3448
2614
  name: "Updated Root",
3449
2615
  description: "Updated description",
3450
2616
  };
3451
2617
  const mockResponse = {
3452
- id: mockTableId,
2618
+ id: QB_TABLE_ID_1,
3453
2619
  name: "Updated Root",
3454
2620
  alias: "_DBID_ROOT",
3455
2621
  description: "Updated description",
@@ -3480,21 +2646,22 @@ tree:
3480
2646
  });
3481
2647
 
3482
2648
  const response = await client.updateTable({
3483
- tableId: mockTableId,
3484
- appId: mockAppId,
2649
+ tableId: QB_TABLE_ID_1,
2650
+ appId: QB_APP_ID,
3485
2651
  body: mockBody,
3486
2652
  });
3487
2653
  expect(response).toEqual(mockResponse);
3488
2654
  expect(mockFetch).toHaveBeenCalledTimes(2);
3489
2655
  expect(mockFetch).toHaveBeenCalledWith(
3490
- `https://api.quickbase.com/v1/auth/temporary/${mockTableId}`,
2656
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
3491
2657
  expect.any(Object)
3492
2658
  );
3493
2659
  expect(mockFetch).toHaveBeenCalledWith(
3494
- `https://api.quickbase.com/v1/tables/${mockTableId}?appId=${mockAppId}`,
2660
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
3495
2661
  expect.objectContaining({
3496
- method: "POST", // Updated to match QuickBase API spec
2662
+ method: "POST",
3497
2663
  headers: expect.objectContaining({
2664
+ "QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
3498
2665
  Authorization: "QB-TEMP-TOKEN temp_token",
3499
2666
  "Content-Type": "application/json",
3500
2667
  }),