quickbase-js 1.1.0-beta.1 → 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/CHANGELOG.md +32 -22
- package/dist/quickbase.d.ts +850 -161
- package/dist/quickbase.js +1054 -275
- package/dist/quickbase.js.map +1 -1
- package/log-gen.yaml +1136 -1969
- package/package.json +1 -1
- package/src/code-generation/fix-spec-definitions.ts +269 -3
- package/src/code-generation/fix-spec-paths.ts +84 -0
- package/src/code-generation/output/quickbase-fixed.json +380 -358
- package/src/generated-unified/QuickbaseClient.ts +5 -4
- package/src/log-gen.ts +5 -3
- package/src/quickbaseClient.ts +35 -41
- package/tests/setup.ts +5 -1
- package/tests/vitest/qb/apps/copyApp.test.ts +76 -0
- package/tests/vitest/qb/apps/createApp.test.ts +64 -0
- package/tests/vitest/qb/apps/deleteApp.test.ts +76 -0
- package/tests/vitest/qb/records/deleteRecords.test.ts +42 -10
- package/tests/vitest/qb/records/runQuery.test.ts +81 -17
- package/tests/vitest/qb/records/upsert.test.ts +33 -3
- package/tests/vitest/qb/tables/getRelationships.test.ts +80 -0
- package/tests/vitest/unit/apps/copyApp.test.ts +282 -0
- package/tests/vitest/unit/apps/createApp.test.ts +125 -0
- package/tests/vitest/unit/apps/deleteApp.test.ts +215 -0
- package/tests/vitest/unit/tables/getRelationships.test.ts +259 -0
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
|
-
|
|
1725
|
-
|
|
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
|
-
|
|
1995
|
-
|
|
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 {
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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/${
|
|
881
|
+
`https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
|
|
2202
882
|
expect.objectContaining({
|
|
2203
883
|
headers: expect.objectContaining({
|
|
2204
|
-
|
|
2205
|
-
|
|
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 {
|
|
2219
|
-
|
|
2220
|
-
|
|
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:
|
|
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 {
|
|
2282
|
-
|
|
2283
|
-
|
|
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:
|
|
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/${
|
|
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": `${
|
|
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:
|
|
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:
|
|
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(
|
|
2354
|
-
(
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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 {
|
|
2367
|
-
|
|
2368
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
2525
|
-
|
|
2526
|
-
|
|
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:
|
|
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
|
|
1387
|
+
`https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}&includeFieldPerms=true`,
|
|
2629
1388
|
expect.objectContaining({
|
|
2630
1389
|
headers: expect.objectContaining({
|
|
2631
|
-
|
|
2632
|
-
|
|
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";
|
|
2653
|
-
import {
|
|
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();
|
|
1424
|
+
mockFetch.mockClear();
|
|
1425
|
+
qb = createClient(mockFetch, { debug: true }); // Use debug: true for consistency with other tests
|
|
2658
1426
|
});
|
|
2659
1427
|
|
|
2660
|
-
test("
|
|
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:
|
|
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":
|
|
2685
|
-
Authorization:
|
|
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:
|
|
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("
|
|
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:
|
|
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":
|
|
2719
|
-
Authorization:
|
|
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:
|
|
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("
|
|
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:
|
|
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: "
|
|
1616
|
+
method: "POST",
|
|
2756
1617
|
headers: expect.objectContaining({
|
|
2757
|
-
"QB-Realm-Hostname":
|
|
2758
|
-
Authorization:
|
|
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 {
|
|
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
|
|
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:
|
|
1667
|
+
body: { to: QB_TABLE_ID_1, data: [{ "6": { value: "value1" } }] },
|
|
2808
1668
|
});
|
|
2809
1669
|
const result = await qb.upsert({
|
|
2810
|
-
body: { to:
|
|
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":
|
|
2820
|
-
Authorization:
|
|
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:
|
|
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:
|
|
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":
|
|
2866
|
-
Authorization:
|
|
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:
|
|
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:
|
|
2907
|
-
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
|
-
|
|
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":
|
|
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":
|
|
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:
|
|
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:
|
|
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":
|
|
2983
|
-
Authorization:
|
|
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:
|
|
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
|
-
|
|
3007
|
-
import {
|
|
3008
|
-
|
|
3009
|
-
|
|
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({
|
|
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=${
|
|
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":
|
|
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
|
-
|
|
3072
|
-
import {
|
|
3073
|
-
|
|
3074
|
-
|
|
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:
|
|
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({
|
|
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/${
|
|
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":
|
|
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(
|
|
3127
|
-
|
|
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/${
|
|
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.
|
|
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
|
-
|
|
3143
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
2049
|
+
spaceRemaining: "500 MB",
|
|
3171
2050
|
},
|
|
3172
2051
|
]),
|
|
3173
2052
|
} as Response);
|
|
3174
2053
|
});
|
|
3175
2054
|
|
|
3176
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2075
|
+
spaceRemaining: "500 MB",
|
|
3200
2076
|
}),
|
|
3201
2077
|
])
|
|
3202
2078
|
);
|
|
3203
2079
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
3204
|
-
`https://api.quickbase.com/v1/tables?appId=${
|
|
2080
|
+
`https://api.quickbase.com/v1/tables?appId=${QB_APP_ID}`,
|
|
3205
2081
|
expect.objectContaining({
|
|
2082
|
+
method: "GET",
|
|
3206
2083
|
headers: expect.objectContaining({
|
|
3207
|
-
|
|
3208
|
-
|
|
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:
|
|
2092
|
+
- name: getRelationships.test.ts
|
|
3215
2093
|
type: file
|
|
3216
2094
|
contents: |
|
|
3217
|
-
|
|
3218
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
3230
|
-
|
|
2376
|
+
client = createClient(mockFetch, { debug: true });
|
|
2377
|
+
|
|
3231
2378
|
const mockResponse = {
|
|
3232
|
-
id:
|
|
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:
|
|
3258
|
-
appId:
|
|
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/${
|
|
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
|
-
|
|
3267
|
-
|
|
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
|
-
|
|
3276
|
-
const mockTableId = "buwai2zr4";
|
|
2422
|
+
|
|
3277
2423
|
const mockResponse = {
|
|
3278
|
-
id:
|
|
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:
|
|
3310
|
-
appId:
|
|
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/${
|
|
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/${
|
|
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
|
-
|
|
3331
|
-
const mockTableId = "buwai2zr4";
|
|
2477
|
+
|
|
3332
2478
|
const mockResponse = {
|
|
3333
|
-
id:
|
|
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:
|
|
3376
|
-
appId:
|
|
2521
|
+
tableId: QB_TABLE_ID_1,
|
|
2522
|
+
appId: QB_APP_ID,
|
|
3377
2523
|
});
|
|
3378
2524
|
expect(response).toEqual(mockResponse);
|
|
3379
|
-
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
3433
|
-
import {
|
|
3434
|
-
|
|
3435
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
3484
|
-
appId:
|
|
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/${
|
|
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/${
|
|
2660
|
+
`https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
|
|
3495
2661
|
expect.objectContaining({
|
|
3496
|
-
method: "POST",
|
|
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
|
}),
|