quickbase-js 0.1.0-beta.8 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +822 -119
  4. package/dist/esm/quickbase.js +373 -236
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +373 -236
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/open-api/generate-unified-interface.ts +97 -17
  13. package/open-api/utils/generateJsDoc.ts +96 -0
  14. package/package.json +3 -1
  15. package/src/ThrottleBucket.ts +17 -12
  16. package/src/authorizationStrategy.ts +246 -0
  17. package/src/generated-unified/QuickbaseClient.ts +809 -119
  18. package/src/invokeMethod.ts +150 -265
  19. package/src/quickbaseClient.ts +85 -58
  20. package/src/rateLimiter.ts +25 -0
  21. package/src/tokenCache.ts +10 -26
  22. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  23. package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
  24. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  25. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  26. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  27. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  28. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  29. package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
  30. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  31. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  32. package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
  33. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
  34. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  35. package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
  36. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  37. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
  38. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  39. package/tests/vitest/unit/fields/createField.test.ts +180 -116
  40. package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
  41. package/tests/vitest/unit/fields/updateField.test.ts +12 -7
  42. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  43. package/tests/vitest/unit/index.ts +2 -1
  44. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +4 -2
  46. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  47. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  48. package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
  49. package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
  50. package/tools/log-gen.ts +14 -5
  51. package/tools/log-gen.yaml +710 -268
  52. package/tests/vitest/unit/auth/retryTempTokenTwo401.test.ts +0 -81
@@ -1,7 +1,7 @@
1
1
  project: >-
2
2
  quickbase-js
3
3
  date: >-
4
- 3/17/2025
4
+ 3/19/2025
5
5
  root: >-
6
6
  /home/drew/Projects/quickbase-js
7
7
  goals:
@@ -888,6 +888,10 @@ tree:
888
888
  type: >-
889
889
  directory
890
890
  children:
891
+ - name: >-
892
+ generateJsDoc.ts
893
+ type: >-
894
+ file
891
895
  - name: >-
892
896
  infer-schema.ts
893
897
  type: >-
@@ -929,6 +933,10 @@ tree:
929
933
  ThrottleBucket.ts
930
934
  type: >-
931
935
  file
936
+ - name: >-
937
+ authorizationStrategy.ts
938
+ type: >-
939
+ file
932
940
  - name: >-
933
941
  generated
934
942
  type: >-
@@ -1642,6 +1650,10 @@ tree:
1642
1650
  quickbaseClient.ts
1643
1651
  type: >-
1644
1652
  file
1653
+ - name: >-
1654
+ rateLimiter.ts
1655
+ type: >-
1656
+ file
1645
1657
  - name: >-
1646
1658
  tokenCache.ts
1647
1659
  type: >-
@@ -1675,6 +1687,10 @@ tree:
1675
1687
  test-umd.js
1676
1688
  type: >-
1677
1689
  file
1690
+ - name: >-
1691
+ test.log
1692
+ type: >-
1693
+ file
1678
1694
  - name: >-
1679
1695
  test.ts
1680
1696
  type: >-
@@ -2276,13 +2292,6 @@ tree:
2276
2292
  from
2277
2293
  "../../../../src/quickbaseClient";
2278
2294
 
2279
- import
2280
- {
2281
- ResponseError
2282
- }
2283
- from
2284
- "../../../../src/generated/runtime";
2285
-
2286
2295
 
2287
2296
  describe("QuickbaseClient
2288
2297
  Integration
@@ -2296,55 +2305,73 @@ tree:
2296
2305
  =>
2297
2306
  {
2298
2307
  let client: QuickbaseClient;
2299
- const mockFetch = vi.fn();
2300
2308
 
2301
2309
  beforeEach(() => {
2310
+ const userToken = process.env.QB_USER_TOKEN;
2311
+ const realm = process.env.QB_REALM;
2312
+ const tableId = process.env.QB_TABLE_ID_1; // Consistent env var name
2313
+
2314
+ if (!userToken) {
2315
+ throw new Error(
2316
+ "QB_USER_TOKEN environment variable is required for integration test"
2317
+ );
2318
+ }
2319
+ if (!tableId) {
2320
+ throw new Error(
2321
+ "QB_TABLE_ID_1 environment variable is required for integration test"
2322
+ );
2323
+ }
2324
+
2302
2325
  const config = {
2303
- realm: process.env.QB_REALM || "builderprogram-dbradford6815",
2304
- userToken: "mock-user-token",
2326
+ realm,
2327
+ userToken,
2305
2328
  debug: true,
2306
- fetchApi: mockFetch,
2307
2329
  };
2308
- console.log("[quickbaseTest] Config:", config);
2330
+ console.log("[quickbaseTest] Config:", {
2331
+ realm,
2332
+ userToken: userToken.substring(0, 10) + "...",
2333
+ tableId, // Log to confirm
2334
+ debug: true,
2335
+ });
2309
2336
  client = quickbase(config);
2310
2337
  });
2311
2338
 
2312
- it("retries on transient 401 with user token and succeeds", async () => {
2313
- const consoleSpy = vi.spyOn(console, "log");
2339
+ it(
2340
+ "successfully retrieves fields with user token, retrying on transient 401 if needed",
2341
+ async () => {
2342
+ const consoleSpy = vi.spyOn(console, "log");
2314
2343
 
2315
- mockFetch
2316
- .mockImplementationOnce(() => {
2317
- const response = new Response(null, {
2318
- status: 401,
2319
- statusText: "Unauthorized",
2320
- });
2321
- console.log("[mockFetch] Throwing 401 ResponseError");
2322
- return Promise.reject(new ResponseError(response));
2323
- })
2324
- .mockResolvedValueOnce({
2325
- ok: true,
2326
- status: 200,
2327
- json: async () => [{ id: 1, label: "Mock Field", fieldType: "text" }],
2328
- });
2344
+ const tableId = process.env.QB_TABLE_ID_1!; // Match beforeEach
2345
+ let fields;
2346
+ try {
2347
+ fields = await client.getFields({ tableId });
2348
+ } catch (error) {
2349
+ console.error("[integrationTest] Failed to retrieve fields:", error);
2350
+ throw error;
2351
+ }
2329
2352
 
2330
- const fields = await client.getFields({ tableId: "mock-table-id" });
2353
+ expect(fields).toBeInstanceOf(Array);
2354
+ expect(fields.length).toBeGreaterThan(0);
2355
+ expect(fields[0]).toHaveProperty("id");
2356
+ expect(fields[0]).toHaveProperty("label");
2331
2357
 
2332
- expect(fields).toBeInstanceOf(Array);
2333
- expect(fields.length).toBeGreaterThan(0);
2334
- expect(fields[0]).toHaveProperty("id");
2335
- expect(fields[0]).toHaveProperty("label");
2358
+ console.log("[integrationTest] Fields retrieved:", fields.length);
2336
2359
 
2337
- expect(consoleSpy).toHaveBeenCalledWith(
2338
- "Authorization error for getFields (user token), retrying with same token:",
2339
- "" // Matches empty ResponseError message from mock
2340
- );
2341
- expect(consoleSpy).toHaveBeenCalledWith(
2342
- "Retrying getFields with user token"
2343
- );
2344
- expect(mockFetch).toHaveBeenCalledTimes(2);
2360
+ const retryLog = consoleSpy.mock.calls.some((call) =>
2361
+ call[0].includes("Retrying getFields with existing user token")
2362
+ );
2363
+ if (retryLog) {
2364
+ console.log("[integrationTest] Retry on 401 detected");
2365
+ } else {
2366
+ console.log(
2367
+ "[integrationTest] No 401 retries observed (expected if no transient failure)"
2368
+ );
2369
+ }
2345
2370
 
2346
- consoleSpy.mockRestore();
2347
- });
2371
+ consoleSpy.mockRestore();
2372
+ },
2373
+ { timeout: 10000 }
2374
+ );
2348
2375
  });
2349
2376
  - name: >-
2350
2377
  fields
@@ -3378,6 +3405,138 @@ tree:
3378
3405
  }
3379
3406
  });
3380
3407
  });
3408
+ - name: >-
3409
+ formulas
3410
+ type: >-
3411
+ directory
3412
+ children:
3413
+ - name: >-
3414
+ runFormula.test.ts
3415
+ type: >-
3416
+ file
3417
+ contents: >
3418
+ //
3419
+ tests/vitest/qb/formulas/runFormula.test.ts
3420
+
3421
+
3422
+ import
3423
+ {
3424
+ describe,
3425
+ it,
3426
+ expect,
3427
+ beforeAll,
3428
+ afterAll
3429
+ }
3430
+ from
3431
+ "vitest";
3432
+
3433
+ import
3434
+ {
3435
+ createClient,
3436
+ QB_REALM,
3437
+ QB_USER_TOKEN,
3438
+ QB_TABLE_ID_1,
3439
+ }
3440
+ from
3441
+ "@tests/setup.ts";
3442
+
3443
+ import
3444
+ {
3445
+ RunFormulaRequest,
3446
+ RunFormula200Response,
3447
+ Upsert200Response,
3448
+ }
3449
+ from
3450
+ "@/generated/models";
3451
+
3452
+
3453
+ describe("QuickbaseClient
3454
+ Integration
3455
+ -
3456
+ runFormula",
3457
+ ()
3458
+ =>
3459
+ {
3460
+ let client: ReturnType<typeof createClient>;
3461
+ let testRecordId: number;
3462
+
3463
+ beforeAll(async () => {
3464
+ // Validate environment variables
3465
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
3466
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
3467
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
3468
+
3469
+ // Initialize client with user token only
3470
+ client = createClient(undefined, {
3471
+ realm: QB_REALM,
3472
+ userToken: QB_USER_TOKEN,
3473
+ debug: true,
3474
+ });
3475
+
3476
+ // Upsert a test record to ensure we have a valid rid
3477
+ const upsertResponse: Upsert200Response = await client.upsert({
3478
+ body: {
3479
+ to: QB_TABLE_ID_1,
3480
+ data: [
3481
+ {
3482
+ "6": { value: `Test Record ${Date.now()}` }, // Assuming field 6 is a text field (e.g., "Task Name")
3483
+ },
3484
+ ],
3485
+ fieldsToReturn: [3], // Return Record ID# (field ID 3)
3486
+ },
3487
+ });
3488
+
3489
+ // Extract the created record ID
3490
+ testRecordId = upsertResponse.data[0]["3"].value as number;
3491
+ console.log(`Upserted test record with ID: ${testRecordId}`);
3492
+ });
3493
+
3494
+ afterAll(async () => {
3495
+ // Clean up the test record
3496
+ if (testRecordId) {
3497
+ await client.deleteRecords({
3498
+ body: {
3499
+ from: QB_TABLE_ID_1,
3500
+ where: `{3.EX.${testRecordId}}`, // Delete by Record ID#
3501
+ },
3502
+ });
3503
+ console.log(`Cleaned up test record with ID: ${testRecordId}`);
3504
+ }
3505
+ });
3506
+
3507
+ it("runs simple formula without rid and returns correct result", async () => {
3508
+ const request: RunFormulaRequest = {
3509
+ from: QB_TABLE_ID_1,
3510
+ formula: "1 + 1", // Simple arithmetic to verify functionality
3511
+ };
3512
+
3513
+ const response: RunFormula200Response = await client.runFormula({
3514
+ body: request,
3515
+ });
3516
+
3517
+ console.log("Real API response (no rid):", response);
3518
+
3519
+ expect(response).toHaveProperty("result");
3520
+ expect(response.result).toBe("2"); // Expect "2" as a string
3521
+ }, 10000);
3522
+
3523
+ it("runs formula with rid and field ID 3, returns correct numeric result", async () => {
3524
+ const request: RunFormulaRequest = {
3525
+ from: QB_TABLE_ID_1,
3526
+ formula: "Sum([Record ID#], 20)", // Field ID 3 is "Record ID#"
3527
+ rid: testRecordId, // Use the upserted record ID
3528
+ };
3529
+
3530
+ const response: RunFormula200Response = await client.runFormula({
3531
+ body: request,
3532
+ });
3533
+
3534
+ console.log("Real API response (with rid):", response);
3535
+
3536
+ expect(response).toHaveProperty("result");
3537
+ expect(response.result).toBe(String(testRecordId + 20)); // Dynamic result based on the record ID
3538
+ }, 10000);
3539
+ });
3381
3540
  - name: >-
3382
3541
  index.ts
3383
3542
  type: >-
@@ -4028,6 +4187,132 @@ tree:
4028
4187
  type: >-
4029
4188
  directory
4030
4189
  children:
4190
+ - name: >-
4191
+ createRelationship.test.ts
4192
+ type: >-
4193
+ file
4194
+ contents: >
4195
+ //
4196
+ tests/vitest/qb/tables/createRelationship.test.ts
4197
+
4198
+
4199
+ import
4200
+ {
4201
+ test,
4202
+ expect
4203
+ }
4204
+ from
4205
+ "vitest";
4206
+
4207
+ import
4208
+ {
4209
+ quickbase
4210
+ }
4211
+ from
4212
+ "/home/drew/Projects/quickbase-js/src/quickbaseClient";
4213
+
4214
+ import
4215
+ {
4216
+ QB_REALM,
4217
+ QB_USER_TOKEN,
4218
+ QB_TABLE_ID_1,
4219
+ QB_TABLE_ID_2,
4220
+ }
4221
+ from
4222
+ "/home/drew/Projects/quickbase-js/tests/setup.ts";
4223
+
4224
+
4225
+ test(
4226
+ "QuickbaseClient Integration - createRelationship > creates a new relationship in QuickBase",
4227
+ { timeout: 60000 }, // 60 seconds for create and delete
4228
+ async () => {
4229
+ // Validate environment variables
4230
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
4231
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
4232
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
4233
+ if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
4234
+
4235
+ // Initialize client
4236
+ const client = quickbase({
4237
+ realm: QB_REALM,
4238
+ userToken: QB_USER_TOKEN,
4239
+ debug: true,
4240
+ });
4241
+
4242
+ // Define the relationship creation request
4243
+ const uniqueLabel = `TestRelationship_${Date.now()}`;
4244
+ const requestBody = {
4245
+ parentTableId: QB_TABLE_ID_1,
4246
+ foreignKeyField: { label: uniqueLabel },
4247
+ lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
4248
+ summaryFields: [
4249
+ {
4250
+ summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
4251
+ label: `Sum_${uniqueLabel}`,
4252
+ accumulationType: "SUM",
4253
+ },
4254
+ ],
4255
+ };
4256
+
4257
+ console.log("Config used:", {
4258
+ realm: QB_REALM,
4259
+ userToken: "[REDACTED]",
4260
+ parentTableId: QB_TABLE_ID_1,
4261
+ childTableId: QB_TABLE_ID_2,
4262
+ });
4263
+ console.log("Creating relationship with:", requestBody);
4264
+
4265
+ // Create the relationship
4266
+ const response = await client.createRelationship({
4267
+ tableId: QB_TABLE_ID_2,
4268
+ body: requestBody,
4269
+ });
4270
+
4271
+ console.log("Real API response:", JSON.stringify(response, null, 2));
4272
+
4273
+ // Assertions for creation
4274
+ expect(response).toBeDefined();
4275
+ expect(response.id).toBeDefined();
4276
+ expect(response.parentTableId).toBe(QB_TABLE_ID_1);
4277
+ expect(response.childTableId).toBe(QB_TABLE_ID_2);
4278
+ expect(response.foreignKeyField).toBeDefined();
4279
+ expect(response.foreignKeyField?.label).toBe(uniqueLabel);
4280
+ expect(response.lookupFields).toHaveLength(1);
4281
+ expect(response.lookupFields[0].id).toBeGreaterThan(0);
4282
+ expect(response.summaryFields).toHaveLength(1);
4283
+ expect(response.summaryFields[0].label).toBe(`Sum_${uniqueLabel}`);
4284
+
4285
+ // Cleanup: Delete the relationship
4286
+ const relationshipId = response.id;
4287
+ console.log("Deleting relationship with ID:", relationshipId);
4288
+ const deleteResponse = await client.deleteRelationship({
4289
+ tableId: QB_TABLE_ID_2,
4290
+ relationshipId,
4291
+ });
4292
+
4293
+ console.log(
4294
+ "Delete relationship response:",
4295
+ JSON.stringify(deleteResponse, null, 2)
4296
+ );
4297
+
4298
+ // Verify deletion
4299
+ expect(deleteResponse).toBeDefined();
4300
+ expect(deleteResponse.relationshipId).toBe(relationshipId);
4301
+
4302
+ // Confirm relationship is gone
4303
+ const relationships = await client.getRelationships({
4304
+ tableId: QB_TABLE_ID_2,
4305
+ });
4306
+ console.log(
4307
+ "Relationships after deletion:",
4308
+ JSON.stringify(relationships, null, 2)
4309
+ );
4310
+ const deletedRelationship = relationships.relationships.find(
4311
+ (r) => r.id === relationshipId
4312
+ );
4313
+ expect(deletedRelationship).toBeUndefined(); // Should no longer exist
4314
+ }
4315
+ );
4031
4316
  - name: >-
4032
4317
  createTable.test.ts
4033
4318
  type: >-
@@ -4101,6 +4386,173 @@ tree:
4101
4386
  console.log("Cleanup - Deleted table:", deleteResponse);
4102
4387
  }
4103
4388
  );
4389
+ - name: >-
4390
+ deleteRelationship.test.ts
4391
+ type: >-
4392
+ file
4393
+ contents: >
4394
+ //
4395
+ tests/vitest/qb/tables/deleteRelationship.test.ts
4396
+
4397
+
4398
+ import
4399
+ {
4400
+ test,
4401
+ expect
4402
+ }
4403
+ from
4404
+ "vitest";
4405
+
4406
+ import
4407
+ {
4408
+ quickbase
4409
+ }
4410
+ from
4411
+ "/home/drew/Projects/quickbase-js/src/quickbaseClient";
4412
+
4413
+ import
4414
+ {
4415
+ QB_REALM,
4416
+ QB_USER_TOKEN,
4417
+ QB_TABLE_ID_1,
4418
+ QB_TABLE_ID_2,
4419
+ }
4420
+ from
4421
+ "/home/drew/Projects/quickbase-js/tests/setup.ts";
4422
+
4423
+
4424
+ test(
4425
+ "QuickbaseClient Integration - deleteRelationship > creates and deletes a relationship in QuickBase",
4426
+ { timeout: 60000 }, // 60 seconds for two API calls
4427
+ async () => {
4428
+ // Validate environment variables
4429
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
4430
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
4431
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
4432
+ if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
4433
+
4434
+ // Initialize client
4435
+ const client = quickbase({
4436
+ realm: QB_REALM,
4437
+ userToken: QB_USER_TOKEN,
4438
+ debug: true,
4439
+ });
4440
+
4441
+ // Step 1: Create a relationship
4442
+ const uniqueLabel = `TestRelationship_${Date.now()}`;
4443
+ const createRequestBody = {
4444
+ parentTableId: QB_TABLE_ID_1,
4445
+ foreignKeyField: { label: uniqueLabel },
4446
+ lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
4447
+ summaryFields: [
4448
+ {
4449
+ summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
4450
+ label: `Sum_${uniqueLabel}`,
4451
+ accumulationType: "SUM",
4452
+ },
4453
+ ],
4454
+ };
4455
+
4456
+ console.log("Config used:", {
4457
+ realm: QB_REALM,
4458
+ userToken: "[REDACTED]",
4459
+ parentTableId: QB_TABLE_ID_1,
4460
+ childTableId: QB_TABLE_ID_2,
4461
+ });
4462
+ console.log("Creating relationship with:", createRequestBody);
4463
+
4464
+ const createResponse = await client.createRelationship({
4465
+ tableId: QB_TABLE_ID_2,
4466
+ body: createRequestBody,
4467
+ });
4468
+
4469
+ console.log(
4470
+ "Created relationship response:",
4471
+ JSON.stringify(createResponse, null, 2)
4472
+ );
4473
+
4474
+ // Verify creation
4475
+ expect(createResponse).toBeDefined();
4476
+ expect(createResponse.id).toBeDefined();
4477
+ expect(createResponse.parentTableId).toBe(QB_TABLE_ID_1);
4478
+ expect(createResponse.childTableId).toBe(QB_TABLE_ID_2);
4479
+ expect(createResponse.foreignKeyField).toBeDefined();
4480
+ expect(createResponse.foreignKeyField?.label).toBe(uniqueLabel);
4481
+
4482
+ const relationshipId = createResponse.id;
4483
+ const foreignKeyFieldId = createResponse.foreignKeyField?.id;
4484
+
4485
+ // Step 2: Delete the relationship
4486
+ console.log("Deleting relationship with ID:", relationshipId);
4487
+ const deleteResponse = await client.deleteRelationship({
4488
+ tableId: QB_TABLE_ID_2,
4489
+ relationshipId,
4490
+ });
4491
+
4492
+ console.log(
4493
+ "Delete relationship response:",
4494
+ JSON.stringify(deleteResponse, null, 2)
4495
+ );
4496
+
4497
+ // Verify deletion (adjusted to match actual response)
4498
+ expect(deleteResponse).toBeDefined();
4499
+ expect(deleteResponse.relationshipId).toBe(relationshipId); // Updated to use "relationshipId"
4500
+
4501
+ // Step 3: Confirm relationship is gone (or non-functional)
4502
+ try {
4503
+ const relationships = await client.getRelationships({
4504
+ tableId: QB_TABLE_ID_2,
4505
+ });
4506
+ console.log(
4507
+ "Relationships after deletion:",
4508
+ JSON.stringify(relationships, null, 2)
4509
+ );
4510
+ // Check if the relationship is still listed (metadata might persist)
4511
+ const deletedRelationship = relationships.relationships.find(
4512
+ (r) => r.id === relationshipId
4513
+ );
4514
+ if (deletedRelationship) {
4515
+ console.log("Relationship metadata persists:", deletedRelationship);
4516
+ expect(deletedRelationship.foreignKeyField).toBeUndefined(); // Should lack fields
4517
+ }
4518
+ } catch (error) {
4519
+ console.error("Error fetching relationships post-deletion:", error);
4520
+ expect(error.message).toMatch(/404|not found/i); // Optional stricter check
4521
+ }
4522
+
4523
+ // Cleanup remaining fields
4524
+ const lookupFieldId = createResponse.lookupFields[0]?.id;
4525
+ const summaryFieldId = createResponse.summaryFields[0]?.id;
4526
+
4527
+ if (foreignKeyFieldId) {
4528
+ await client.deleteFields({
4529
+ tableId: QB_TABLE_ID_2,
4530
+ body: { fieldIds: [foreignKeyFieldId] },
4531
+ });
4532
+ console.log(
4533
+ `Deleted foreign key field ${foreignKeyFieldId} from ${QB_TABLE_ID_2}`
4534
+ );
4535
+ }
4536
+ if (lookupFieldId) {
4537
+ await client.deleteFields({
4538
+ tableId: QB_TABLE_ID_2,
4539
+ body: { fieldIds: [lookupFieldId] },
4540
+ });
4541
+ console.log(
4542
+ `Deleted lookup field ${lookupFieldId} from ${QB_TABLE_ID_2}`
4543
+ );
4544
+ }
4545
+ if (summaryFieldId) {
4546
+ await client.deleteFields({
4547
+ tableId: QB_TABLE_ID_1,
4548
+ body: { fieldIds: [summaryFieldId] },
4549
+ });
4550
+ console.log(
4551
+ `Deleted summary field ${summaryFieldId} from ${QB_TABLE_ID_1}`
4552
+ );
4553
+ }
4554
+ }
4555
+ );
4104
4556
  - name: >-
4105
4557
  deleteTable.test.ts
4106
4558
  type: >-
@@ -4409,10 +4861,7 @@ tree:
4409
4861
  {
4410
4862
  describe,
4411
4863
  it,
4412
- expect,
4413
- beforeAll,
4414
- afterAll,
4415
- beforeEach
4864
+ expect
4416
4865
  }
4417
4866
  from
4418
4867
  "vitest";
@@ -4446,38 +4895,30 @@ tree:
4446
4895
  {
4447
4896
  let client: ReturnType<typeof createClient>;
4448
4897
  let relationshipId: number;
4449
- let newLookupFieldId: number;
4450
- let newChildFieldId: number;
4898
+ let newLookupFieldId: number | undefined;
4899
+ let newChildFieldId: number | undefined;
4900
+ let generatedLookupFieldId: number | undefined;
4901
+ let generatedSummaryFieldId: number | undefined;
4451
4902
 
4452
- beforeAll(async () => {
4453
- client = createClient(fetch, {
4454
- realm: QB_REALM,
4455
- userToken: QB_USER_TOKEN,
4456
- debug: true,
4457
- });
4903
+ // No beforeAll/afterAll since we'll create and delete within the test
4458
4904
 
4905
+ it("creates, updates, deletes, and verifies a relationship", async () => {
4906
+ // Validate environment variables
4459
4907
  if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
4460
4908
  if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
4909
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
4910
+ if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
4461
4911
 
4462
- // Fetch relationship ID
4463
- const relationshipsResponse = await client.getRelationships({
4464
- tableId: QB_TABLE_ID_2,
4465
- skip: 0,
4912
+ // Initialize client
4913
+ client = createClient(fetch, {
4914
+ realm: QB_REALM,
4915
+ userToken: QB_USER_TOKEN,
4916
+ debug: true,
4466
4917
  });
4467
4918
 
4468
- const relationship = relationshipsResponse.relationships.find(
4469
- (r) => r.parentTableId === QB_TABLE_ID_1
4470
- );
4471
- if (!relationship) {
4472
- throw new Error(
4473
- `No relationship found between parent ${QB_TABLE_ID_1} and child ${QB_TABLE_ID_2}`
4474
- );
4475
- }
4476
- relationshipId = relationship.id;
4477
-
4478
- // Create fields
4919
+ // Step 1: Create fields for the relationship update
4479
4920
  const lookupFieldRequest: CreateFieldRequest = {
4480
- label: "Test Lookup Field",
4921
+ label: `Test Lookup Field_${Date.now()}`,
4481
4922
  fieldType: "text",
4482
4923
  };
4483
4924
  const lookupResponse = await client.createField({
@@ -4485,9 +4926,15 @@ tree:
4485
4926
  body: lookupFieldRequest,
4486
4927
  });
4487
4928
  newLookupFieldId = lookupResponse.id;
4929
+ if (!newLookupFieldId) {
4930
+ throw new Error("Failed to retrieve ID for newly created lookup field");
4931
+ }
4932
+ console.log(
4933
+ `Created lookup field with ID ${newLookupFieldId} in parent table ${QB_TABLE_ID_1}`
4934
+ );
4488
4935
 
4489
4936
  const childFieldRequest: CreateFieldRequest = {
4490
- label: "Test Child Numeric",
4937
+ label: `Test Child Numeric_${Date.now()}`,
4491
4938
  fieldType: "numeric",
4492
4939
  };
4493
4940
  const childResponse = await client.createField({
@@ -4495,224 +4942,190 @@ tree:
4495
4942
  body: childFieldRequest,
4496
4943
  });
4497
4944
  newChildFieldId = childResponse.id;
4498
-
4499
- console.log(
4500
- `Created lookup field ${newLookupFieldId} in parent ${QB_TABLE_ID_1}`
4501
- );
4502
- console.log(
4503
- `Created child field ${newChildFieldId} in child ${QB_TABLE_ID_2}`
4504
- );
4945
+ if (!newChildFieldId) {
4946
+ throw new Error("Failed to retrieve ID for newly created child field");
4947
+ }
4505
4948
  console.log(
4506
- `Using relationshipId: ${relationshipId} from child ${QB_TABLE_ID_2} to parent ${QB_TABLE_ID_1}`
4949
+ `Created child field with ID ${newChildFieldId} in child table ${QB_TABLE_ID_2}`
4507
4950
  );
4508
- }, 30000);
4509
4951
 
4510
- afterAll(async () => {
4511
- if (newLookupFieldId) {
4512
- await client.deleteFields({
4513
- tableId: QB_TABLE_ID_1,
4514
- body: { fieldIds: [newLookupFieldId] },
4515
- });
4516
- console.log(`Deleted field ${newLookupFieldId} from ${QB_TABLE_ID_1}`);
4517
- }
4518
- if (newChildFieldId) {
4519
- await client.deleteFields({
4520
- tableId: QB_TABLE_ID_2,
4521
- body: { fieldIds: [newChildFieldId] },
4522
- });
4523
- console.log(`Deleted field ${newChildFieldId} from ${QB_TABLE_ID_2}`);
4524
- }
4525
- }, 30000);
4952
+ // Step 2: Create a new relationship
4953
+ const uniqueLabel = `TestRelationship_${Date.now()}`;
4954
+ const createRequestBody = {
4955
+ parentTableId: QB_TABLE_ID_1,
4956
+ foreignKeyField: { label: uniqueLabel },
4957
+ lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
4958
+ summaryFields: [
4959
+ {
4960
+ summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
4961
+ label: `Sum_${uniqueLabel}`,
4962
+ accumulationType: "SUM",
4963
+ },
4964
+ ],
4965
+ };
4526
4966
 
4527
- beforeEach(async () => {
4528
- // Reset relationship to a clean state before each test
4529
- const relationshipsResponse = await client.getRelationships({
4967
+ console.log("Creating relationship with:", createRequestBody);
4968
+ const createResponse = await client.createRelationship({
4530
4969
  tableId: QB_TABLE_ID_2,
4970
+ body: createRequestBody,
4531
4971
  });
4532
- const relationship = relationshipsResponse.relationships.find(
4533
- (r) => r.id === relationshipId
4972
+ console.log(
4973
+ "Created relationship response:",
4974
+ JSON.stringify(createResponse, null, 2)
4534
4975
  );
4535
- const cleanupRequest: UpdateRelationshipRequest = {
4536
- lookupFieldIds: (relationship?.lookupFields || [])
4537
- .map((f) => f.id)
4538
- .filter((id) => id !== newLookupFieldId),
4539
- summaryFields: (relationship?.summaryFields || []).map((f) => ({
4540
- summaryFid: f.id,
4541
- label: f.label,
4542
- accumulationType: "COUNT", // Assuming COUNT for existing fields; adjust if needed
4543
- })),
4544
- };
4545
- await client.updateRelationship({
4546
- tableId: QB_TABLE_ID_2,
4547
- relationshipId,
4548
- body: cleanupRequest,
4549
- });
4550
- console.log(`Reset relationship ${relationshipId} before test`);
4551
- });
4552
-
4553
- it(
4554
- "adds a lookup field to the relationship",
4555
- async ({ expect }) => {
4556
- const tableId = QB_TABLE_ID_2;
4557
4976
 
4558
- // Get current state
4559
- const initialResponse = await client.getRelationships({ tableId });
4560
- const initialRelationship = initialResponse.relationships.find(
4561
- (r) => r.id === relationshipId
4562
- );
4563
- const currentLookupFields = (initialRelationship?.lookupFields || []).map(
4564
- (f) => f.id
4565
- );
4566
-
4567
- const request: UpdateRelationshipRequest = {
4568
- lookupFieldIds: [...currentLookupFields, newLookupFieldId],
4569
- };
4570
-
4571
- const updateResponse = await client.updateRelationship({
4572
- tableId,
4573
- relationshipId,
4574
- body: request,
4575
- });
4977
+ expect(createResponse).toBeDefined();
4978
+ expect(createResponse.id).toBeDefined();
4979
+ expect(createResponse.parentTableId).toBe(QB_TABLE_ID_1);
4980
+ expect(createResponse.childTableId).toBe(QB_TABLE_ID_2);
4981
+ expect(createResponse.foreignKeyField?.label).toBe(uniqueLabel);
4576
4982
 
4577
- expect(updateResponse.id).toBe(relationshipId);
4578
- expect(updateResponse.childTableId).toBe(QB_TABLE_ID_2);
4579
- expect(updateResponse.parentTableId).toBe(QB_TABLE_ID_1);
4580
- expect(
4581
- updateResponse.lookupFields.some((f) => f.id === newLookupFieldId)
4582
- ).toBe(true);
4983
+ relationshipId = createResponse.id;
4583
4984
 
4584
- const relationshipsResponse = await client.getRelationships({ tableId });
4585
- const updatedRelationship = relationshipsResponse.relationships.find(
4586
- (r) => r.id === relationshipId
4587
- );
4588
- expect(
4589
- updatedRelationship!.lookupFields.some((f) => f.id === newLookupFieldId)
4590
- ).toBe(true);
4591
- },
4592
- { timeout: 30000 }
4593
- );
4985
+ // Step 3: Update the relationship - Add a lookup field
4986
+ const tableId = QB_TABLE_ID_2;
4987
+ const initialResponse = await client.getRelationships({ tableId });
4988
+ const initialRelationship = initialResponse.relationships.find(
4989
+ (r) => r.id === relationshipId
4990
+ );
4991
+ const currentLookupFields = (initialRelationship?.lookupFields || []).map(
4992
+ (f) => f.id
4993
+ );
4994
+ console.log("Current lookupFields before update:", currentLookupFields);
4594
4995
 
4595
- it(
4596
- "adds a summary field to the parent table",
4597
- async ({ expect }) => {
4598
- const tableId = QB_TABLE_ID_2;
4996
+ const updateLookupRequest: UpdateRelationshipRequest = {
4997
+ lookupFieldIds: [newLookupFieldId!],
4998
+ };
4999
+ console.log(
5000
+ "Adding lookup field request:",
5001
+ JSON.stringify(updateLookupRequest, null, 2)
5002
+ );
4599
5003
 
4600
- // Get current state
4601
- const initialResponse = await client.getRelationships({ tableId });
4602
- const initialRelationship = initialResponse.relationships.find(
4603
- (r) => r.id === relationshipId
4604
- );
4605
- const currentSummaryFields = (
4606
- initialRelationship?.summaryFields || []
4607
- ).map((f) => ({
4608
- summaryFid: f.id,
4609
- label: f.label,
4610
- accumulationType: "COUNT",
4611
- }));
4612
-
4613
- const request: UpdateRelationshipRequest = {
4614
- summaryFields: [
4615
- ...currentSummaryFields,
4616
- {
4617
- summaryFid: newChildFieldId,
4618
- label: "Test Summary Field",
4619
- accumulationType: "SUM",
4620
- },
4621
- ],
4622
- };
5004
+ const updateLookupResponse = await client.updateRelationship({
5005
+ tableId,
5006
+ relationshipId,
5007
+ body: updateLookupRequest,
5008
+ });
5009
+ console.log(
5010
+ "Update lookup response:",
5011
+ JSON.stringify(updateLookupResponse, null, 2)
5012
+ );
4623
5013
 
4624
- const updateResponse = await client.updateRelationship({
4625
- tableId,
4626
- relationshipId,
4627
- body: request,
4628
- });
5014
+ const newLookup = updateLookupResponse.lookupFields.find(
5015
+ (f) =>
5016
+ f.label.includes("Test Lookup Field") &&
5017
+ !currentLookupFields.includes(f.id)
5018
+ );
5019
+ generatedLookupFieldId = newLookup?.id;
5020
+ console.log(
5021
+ `Captured generated lookup field ID: ${generatedLookupFieldId}`
5022
+ );
4629
5023
 
4630
- expect(updateResponse.id).toBe(relationshipId);
4631
- expect(updateResponse.childTableId).toBe(QB_TABLE_ID_2);
4632
- expect(updateResponse.parentTableId).toBe(QB_TABLE_ID_1);
4633
- expect(
4634
- updateResponse.summaryFields.some(
4635
- (f) => f.label === "Test Summary Field"
4636
- )
4637
- ).toBe(true);
5024
+ expect(updateLookupResponse.id).toBe(relationshipId);
5025
+ expect(updateLookupResponse.childTableId).toBe(QB_TABLE_ID_2);
5026
+ expect(updateLookupResponse.parentTableId).toBe(QB_TABLE_ID_1);
5027
+ expect(
5028
+ updateLookupResponse.lookupFields.some((f) =>
5029
+ f.label.includes("Test Lookup Field")
5030
+ )
5031
+ ).toBe(true);
5032
+
5033
+ // Step 4: Update the relationship - Add a summary field
5034
+ const currentSummaryFields = (updateLookupResponse.summaryFields || []).map(
5035
+ (f) => f.id
5036
+ );
5037
+ console.log("Current summaryFields before update:", currentSummaryFields);
4638
5038
 
4639
- const relationshipsResponse = await client.getRelationships({ tableId });
4640
- const updatedRelationship = relationshipsResponse.relationships.find(
4641
- (r) => r.id === relationshipId
4642
- );
4643
- expect(
4644
- updatedRelationship!.summaryFields.some(
4645
- (f) => f.label === "Test Summary Field"
4646
- )
4647
- ).toBe(true);
4648
- },
4649
- { timeout: 30000 }
4650
- );
5039
+ const updateSummaryRequest: UpdateRelationshipRequest = {
5040
+ summaryFields: [
5041
+ {
5042
+ summaryFid: newChildFieldId!,
5043
+ label: "Test Summary Field",
5044
+ accumulationType: "SUM",
5045
+ },
5046
+ ],
5047
+ };
5048
+ console.log(
5049
+ "Adding summary field request:",
5050
+ JSON.stringify(updateSummaryRequest, null, 2)
5051
+ );
4651
5052
 
4652
- it(
4653
- "handles an invalid field ID gracefully",
4654
- async ({ expect }) => {
4655
- const tableId = QB_TABLE_ID_2;
5053
+ const updateSummaryResponse = await client.updateRelationship({
5054
+ tableId,
5055
+ relationshipId,
5056
+ body: updateSummaryRequest,
5057
+ });
5058
+ console.log(
5059
+ "Update summary response:",
5060
+ JSON.stringify(updateSummaryResponse, null, 2)
5061
+ );
4656
5062
 
4657
- const request: UpdateRelationshipRequest = {
4658
- lookupFieldIds: [999999], // Invalid ID
4659
- };
5063
+ const newSummary = updateSummaryResponse.summaryFields.find(
5064
+ (f) =>
5065
+ f.label.includes("Test Summary Field") &&
5066
+ !currentSummaryFields.includes(f.id)
5067
+ );
5068
+ generatedSummaryFieldId = newSummary?.id;
5069
+ console.log(
5070
+ `Captured generated summary field ID: ${generatedSummaryFieldId}`
5071
+ );
4660
5072
 
4661
- await expect(
4662
- client.updateRelationship({
4663
- tableId,
4664
- relationshipId,
4665
- body: request,
4666
- })
4667
- ).rejects.toThrow(/API Error:.*400/);
4668
- },
4669
- { timeout: 30000 }
4670
- );
5073
+ expect(updateSummaryResponse.id).toBe(relationshipId);
5074
+ expect(updateSummaryResponse.childTableId).toBe(QB_TABLE_ID_2);
5075
+ expect(updateSummaryResponse.parentTableId).toBe(QB_TABLE_ID_1);
5076
+ expect(
5077
+ updateSummaryResponse.summaryFields.some((f) =>
5078
+ f.label.includes("Test Summary Field")
5079
+ )
5080
+ ).toBe(true);
5081
+
5082
+ // Step 5: Delete the relationship
5083
+ console.log("Deleting relationship with ID:", relationshipId);
5084
+ const deleteResponse = await client.deleteRelationship({
5085
+ tableId: QB_TABLE_ID_2,
5086
+ relationshipId,
5087
+ });
5088
+ console.log(
5089
+ "Delete relationship response:",
5090
+ JSON.stringify(deleteResponse, null, 2)
5091
+ );
4671
5092
 
4672
- it(
4673
- "succeeds with no changes (idempotent)",
4674
- async ({ expect }) => {
4675
- const tableId = QB_TABLE_ID_2;
5093
+ expect(deleteResponse).toBeDefined();
5094
+ expect(deleteResponse.relationshipId).toBe(relationshipId);
4676
5095
 
4677
- // Get current state
4678
- const initialResponse = await client.getRelationships({ tableId });
4679
- const initialRelationship = initialResponse.relationships.find(
4680
- (r) => r.id === relationshipId
4681
- );
4682
- const currentLookupFields = (initialRelationship?.lookupFields || []).map(
4683
- (f) => f.id
4684
- );
4685
- const currentSummaryFields = (
4686
- initialRelationship?.summaryFields || []
4687
- ).map((f) => ({
4688
- summaryFid: f.id,
4689
- label: f.label,
4690
- accumulationType: "COUNT",
4691
- }));
4692
-
4693
- const request: UpdateRelationshipRequest = {
4694
- lookupFieldIds: currentLookupFields,
4695
- summaryFields: currentSummaryFields,
4696
- };
5096
+ // Step 6: Verify deletion
5097
+ const relationships = await client.getRelationships({
5098
+ tableId: QB_TABLE_ID_2,
5099
+ });
5100
+ console.log(
5101
+ "Relationships after deletion:",
5102
+ JSON.stringify(relationships, null, 2)
5103
+ );
5104
+ const deletedRelationship = relationships.relationships.find(
5105
+ (r) => r.id === relationshipId
5106
+ );
5107
+ expect(deletedRelationship).toBeUndefined(); // Confirm relationship is gone
4697
5108
 
4698
- const updateResponse = await client.updateRelationship({
4699
- tableId,
4700
- relationshipId,
4701
- body: request,
5109
+ // Cleanup remaining fields from setup
5110
+ if (newLookupFieldId) {
5111
+ await client.deleteFields({
5112
+ tableId: QB_TABLE_ID_1,
5113
+ body: { fieldIds: [newLookupFieldId] },
4702
5114
  });
4703
-
4704
- expect(updateResponse.id).toBe(relationshipId);
4705
- expect(updateResponse.childTableId).toBe(QB_TABLE_ID_2);
4706
- expect(updateResponse.parentTableId).toBe(QB_TABLE_ID_1);
4707
- expect(updateResponse.lookupFields.map((f) => f.id)).toEqual(
4708
- currentLookupFields
5115
+ console.log(
5116
+ `Deleted lookup field ${newLookupFieldId} from ${QB_TABLE_ID_1}`
4709
5117
  );
4710
- expect(updateResponse.summaryFields?.map((f) => f.id) || []).toEqual(
4711
- currentSummaryFields.map((f) => f.summaryFid)
5118
+ }
5119
+ if (newChildFieldId) {
5120
+ await client.deleteFields({
5121
+ tableId: QB_TABLE_ID_2,
5122
+ body: { fieldIds: [newChildFieldId] },
5123
+ });
5124
+ console.log(
5125
+ `Deleted child field ${newChildFieldId} from ${QB_TABLE_ID_2}`
4712
5126
  );
4713
- },
4714
- { timeout: 30000 }
4715
- );
5127
+ }
5128
+ }, 60000);
4716
5129
  });
4717
5130
  - name: >-
4718
5131
  updateTable.test.ts
@@ -4785,6 +5198,10 @@ tree:
4785
5198
  type: >-
4786
5199
  directory
4787
5200
  children:
5201
+ - name: >-
5202
+ ThrottleBucket.test.ts
5203
+ type: >-
5204
+ file
4788
5205
  - name: >-
4789
5206
  apps
4790
5207
  type: >-
@@ -4832,13 +5249,17 @@ tree:
4832
5249
  type: >-
4833
5250
  file
4834
5251
  - name: >-
4835
- retryTempTokenTwo401.test.ts
5252
+ retryTempTokenMaxRetries.test.ts
4836
5253
  type: >-
4837
5254
  file
4838
5255
  - name: >-
4839
5256
  retryUserToken401.test.ts
4840
5257
  type: >-
4841
5258
  file
5259
+ - name: >-
5260
+ ssoTokenRefresh.test.ts
5261
+ type: >-
5262
+ file
4842
5263
  - name: >-
4843
5264
  fields
4844
5265
  type: >-
@@ -4872,10 +5293,23 @@ tree:
4872
5293
  updateField.test.ts
4873
5294
  type: >-
4874
5295
  file
5296
+ - name: >-
5297
+ formulas
5298
+ type: >-
5299
+ directory
5300
+ children:
5301
+ - name: >-
5302
+ runFormula.test.ts
5303
+ type: >-
5304
+ file
4875
5305
  - name: >-
4876
5306
  index.ts
4877
5307
  type: >-
4878
5308
  file
5309
+ - name: >-
5310
+ inferHttpMethod.test.ts
5311
+ type: >-
5312
+ file
4879
5313
  - name: >-
4880
5314
  rateLimit.test.ts
4881
5315
  type: >-
@@ -4902,10 +5336,18 @@ tree:
4902
5336
  type: >-
4903
5337
  directory
4904
5338
  children:
5339
+ - name: >-
5340
+ createRelationship.test.ts
5341
+ type: >-
5342
+ file
4905
5343
  - name: >-
4906
5344
  createTable.test.ts
4907
5345
  type: >-
4908
5346
  file
5347
+ - name: >-
5348
+ deleteRelationship.test.ts
5349
+ type: >-
5350
+ file
4909
5351
  - name: >-
4910
5352
  deleteTable.test.ts
4911
5353
  type: >-