quickbase-js 0.1.0-beta.7 → 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 (82) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +1292 -125
  4. package/dist/esm/quickbase.js +666 -190
  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 +674 -189
  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/fix-spec-main.ts +69 -68
  13. package/open-api/generate-override-report.ts +214 -0
  14. package/open-api/generate-unified-interface.ts +162 -103
  15. package/open-api/output/override-report.html +11 -0
  16. package/open-api/output/override-report.json +14395 -58
  17. package/open-api/output/quickbase-fixed.json +583 -2
  18. package/open-api/schema/tags/fields.ts +331 -16
  19. package/open-api/schema/tags/index.ts +2 -4
  20. package/open-api/types/spec.ts +2 -1
  21. package/open-api/utils/generateJsDoc.ts +96 -0
  22. package/package.json +33 -33
  23. package/src/RateLimitError.ts +12 -0
  24. package/src/ThrottleBucket.ts +41 -0
  25. package/src/authorizationStrategy.ts +246 -0
  26. package/src/generated-unified/QuickbaseClient.ts +809 -119
  27. package/src/invokeMethod.ts +242 -0
  28. package/src/quickbaseClient.ts +117 -248
  29. package/src/rateLimiter.ts +25 -0
  30. package/src/tokenCache.ts +10 -26
  31. package/tests/setup.ts +2 -0
  32. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  33. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  34. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  35. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  36. package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
  37. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  38. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  39. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  40. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  41. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  42. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  43. package/tests/vitest/qb/index.ts +1 -0
  44. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  45. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  46. package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
  47. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  48. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  49. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  50. package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
  51. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
  52. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  53. package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
  54. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  55. package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
  56. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  57. package/tests/vitest/unit/fields/createField.test.ts +180 -120
  58. package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
  59. package/tests/vitest/unit/fields/updateField.test.ts +285 -0
  60. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  61. package/tests/vitest/unit/index.ts +2 -1
  62. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  63. package/tests/vitest/unit/rateLimit.test.ts +191 -0
  64. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  65. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  66. package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
  67. package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
  68. package/tools/log-gen.ts +20 -7
  69. package/tools/log-gen.yaml +3446 -1848
  70. package/tsconfig.json +5 -2
  71. package/vitest.config.ts +2 -2
  72. package/notes.txt +0 -158
  73. package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
  74. package/tests/vitest/unit/auth/two401s.test.ts +0 -86
  75. package/tools/generate-types.ts +0 -29
  76. package/tools/openapi-schema-validator.ts +0 -38
  77. package/tools/spec-analyzer.ts +0 -163
  78. package/tools/split-spec.ts +0 -148
  79. package/tools/test-import.ts +0 -3
  80. package/tools/validate-spec.ts +0 -40
  81. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  82. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -0,0 +1,96 @@
1
+ // tests/vitest/qb/formulas/runFormula.test.ts
2
+
3
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
4
+ import {
5
+ createClient,
6
+ QB_REALM,
7
+ QB_USER_TOKEN,
8
+ QB_TABLE_ID_1,
9
+ } from "@tests/setup.ts";
10
+ import {
11
+ RunFormulaRequest,
12
+ RunFormula200Response,
13
+ Upsert200Response,
14
+ } from "@/generated/models";
15
+
16
+ describe("QuickbaseClient Integration - runFormula", () => {
17
+ let client: ReturnType<typeof createClient>;
18
+ let testRecordId: number;
19
+
20
+ beforeAll(async () => {
21
+ // Validate environment variables
22
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
23
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
24
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
25
+
26
+ // Initialize client with user token only
27
+ client = createClient(undefined, {
28
+ realm: QB_REALM,
29
+ userToken: QB_USER_TOKEN,
30
+ debug: true,
31
+ });
32
+
33
+ // Upsert a test record to ensure we have a valid rid
34
+ const upsertResponse: Upsert200Response = await client.upsert({
35
+ body: {
36
+ to: QB_TABLE_ID_1,
37
+ data: [
38
+ {
39
+ "6": { value: `Test Record ${Date.now()}` }, // Assuming field 6 is a text field (e.g., "Task Name")
40
+ },
41
+ ],
42
+ fieldsToReturn: [3], // Return Record ID# (field ID 3)
43
+ },
44
+ });
45
+
46
+ // Extract the created record ID
47
+ testRecordId = upsertResponse.data[0]["3"].value as number;
48
+ console.log(`Upserted test record with ID: ${testRecordId}`);
49
+ });
50
+
51
+ afterAll(async () => {
52
+ // Clean up the test record
53
+ if (testRecordId) {
54
+ await client.deleteRecords({
55
+ body: {
56
+ from: QB_TABLE_ID_1,
57
+ where: `{3.EX.${testRecordId}}`, // Delete by Record ID#
58
+ },
59
+ });
60
+ console.log(`Cleaned up test record with ID: ${testRecordId}`);
61
+ }
62
+ });
63
+
64
+ it("runs simple formula without rid and returns correct result", async () => {
65
+ const request: RunFormulaRequest = {
66
+ from: QB_TABLE_ID_1,
67
+ formula: "1 + 1", // Simple arithmetic to verify functionality
68
+ };
69
+
70
+ const response: RunFormula200Response = await client.runFormula({
71
+ body: request,
72
+ });
73
+
74
+ console.log("Real API response (no rid):", response);
75
+
76
+ expect(response).toHaveProperty("result");
77
+ expect(response.result).toBe("2"); // Expect "2" as a string
78
+ }, 10000);
79
+
80
+ it("runs formula with rid and field ID 3, returns correct numeric result", async () => {
81
+ const request: RunFormulaRequest = {
82
+ from: QB_TABLE_ID_1,
83
+ formula: "Sum([Record ID#], 20)", // Field ID 3 is "Record ID#"
84
+ rid: testRecordId, // Use the upserted record ID
85
+ };
86
+
87
+ const response: RunFormula200Response = await client.runFormula({
88
+ body: request,
89
+ });
90
+
91
+ console.log("Real API response (with rid):", response);
92
+
93
+ expect(response).toHaveProperty("result");
94
+ expect(response.result).toBe(String(testRecordId + 20)); // Dynamic result based on the record ID
95
+ }, 10000);
96
+ });
@@ -26,3 +26,4 @@ export * from "./tables/getAppTables.test.ts";
26
26
  export * from "./tables/getRelationships.test.ts";
27
27
  export * from "./tables/getTable.test.ts";
28
28
  export * from "./tables/updateTable.test.ts";
29
+ export * from "./tables/updateRelationship.test.ts";
@@ -0,0 +1,102 @@
1
+ // tests/vitest/qb/tables/createRelationship.test.ts
2
+
3
+ import { test, expect } from "vitest";
4
+ import { quickbase } from "/home/drew/Projects/quickbase-js/src/quickbaseClient";
5
+ import {
6
+ QB_REALM,
7
+ QB_USER_TOKEN,
8
+ QB_TABLE_ID_1,
9
+ QB_TABLE_ID_2,
10
+ } from "/home/drew/Projects/quickbase-js/tests/setup.ts";
11
+
12
+ test(
13
+ "QuickbaseClient Integration - createRelationship > creates a new relationship in QuickBase",
14
+ { timeout: 60000 }, // 60 seconds for create and delete
15
+ async () => {
16
+ // Validate environment variables
17
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
18
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
19
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
20
+ if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
21
+
22
+ // Initialize client
23
+ const client = quickbase({
24
+ realm: QB_REALM,
25
+ userToken: QB_USER_TOKEN,
26
+ debug: true,
27
+ });
28
+
29
+ // Define the relationship creation request
30
+ const uniqueLabel = `TestRelationship_${Date.now()}`;
31
+ const requestBody = {
32
+ parentTableId: QB_TABLE_ID_1,
33
+ foreignKeyField: { label: uniqueLabel },
34
+ lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
35
+ summaryFields: [
36
+ {
37
+ summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
38
+ label: `Sum_${uniqueLabel}`,
39
+ accumulationType: "SUM",
40
+ },
41
+ ],
42
+ };
43
+
44
+ console.log("Config used:", {
45
+ realm: QB_REALM,
46
+ userToken: "[REDACTED]",
47
+ parentTableId: QB_TABLE_ID_1,
48
+ childTableId: QB_TABLE_ID_2,
49
+ });
50
+ console.log("Creating relationship with:", requestBody);
51
+
52
+ // Create the relationship
53
+ const response = await client.createRelationship({
54
+ tableId: QB_TABLE_ID_2,
55
+ body: requestBody,
56
+ });
57
+
58
+ console.log("Real API response:", JSON.stringify(response, null, 2));
59
+
60
+ // Assertions for creation
61
+ expect(response).toBeDefined();
62
+ expect(response.id).toBeDefined();
63
+ expect(response.parentTableId).toBe(QB_TABLE_ID_1);
64
+ expect(response.childTableId).toBe(QB_TABLE_ID_2);
65
+ expect(response.foreignKeyField).toBeDefined();
66
+ expect(response.foreignKeyField?.label).toBe(uniqueLabel);
67
+ expect(response.lookupFields).toHaveLength(1);
68
+ expect(response.lookupFields[0].id).toBeGreaterThan(0);
69
+ expect(response.summaryFields).toHaveLength(1);
70
+ expect(response.summaryFields[0].label).toBe(`Sum_${uniqueLabel}`);
71
+
72
+ // Cleanup: Delete the relationship
73
+ const relationshipId = response.id;
74
+ console.log("Deleting relationship with ID:", relationshipId);
75
+ const deleteResponse = await client.deleteRelationship({
76
+ tableId: QB_TABLE_ID_2,
77
+ relationshipId,
78
+ });
79
+
80
+ console.log(
81
+ "Delete relationship response:",
82
+ JSON.stringify(deleteResponse, null, 2)
83
+ );
84
+
85
+ // Verify deletion
86
+ expect(deleteResponse).toBeDefined();
87
+ expect(deleteResponse.relationshipId).toBe(relationshipId);
88
+
89
+ // Confirm relationship is gone
90
+ const relationships = await client.getRelationships({
91
+ tableId: QB_TABLE_ID_2,
92
+ });
93
+ console.log(
94
+ "Relationships after deletion:",
95
+ JSON.stringify(relationships, null, 2)
96
+ );
97
+ const deletedRelationship = relationships.relationships.find(
98
+ (r) => r.id === relationshipId
99
+ );
100
+ expect(deletedRelationship).toBeUndefined(); // Should no longer exist
101
+ }
102
+ );
@@ -0,0 +1,143 @@
1
+ // tests/vitest/qb/tables/deleteRelationship.test.ts
2
+
3
+ import { test, expect } from "vitest";
4
+ import { quickbase } from "/home/drew/Projects/quickbase-js/src/quickbaseClient";
5
+ import {
6
+ QB_REALM,
7
+ QB_USER_TOKEN,
8
+ QB_TABLE_ID_1,
9
+ QB_TABLE_ID_2,
10
+ } from "/home/drew/Projects/quickbase-js/tests/setup.ts";
11
+
12
+ test(
13
+ "QuickbaseClient Integration - deleteRelationship > creates and deletes a relationship in QuickBase",
14
+ { timeout: 60000 }, // 60 seconds for two API calls
15
+ async () => {
16
+ // Validate environment variables
17
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
18
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
19
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
20
+ if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
21
+
22
+ // Initialize client
23
+ const client = quickbase({
24
+ realm: QB_REALM,
25
+ userToken: QB_USER_TOKEN,
26
+ debug: true,
27
+ });
28
+
29
+ // Step 1: Create a relationship
30
+ const uniqueLabel = `TestRelationship_${Date.now()}`;
31
+ const createRequestBody = {
32
+ parentTableId: QB_TABLE_ID_1,
33
+ foreignKeyField: { label: uniqueLabel },
34
+ lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
35
+ summaryFields: [
36
+ {
37
+ summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
38
+ label: `Sum_${uniqueLabel}`,
39
+ accumulationType: "SUM",
40
+ },
41
+ ],
42
+ };
43
+
44
+ console.log("Config used:", {
45
+ realm: QB_REALM,
46
+ userToken: "[REDACTED]",
47
+ parentTableId: QB_TABLE_ID_1,
48
+ childTableId: QB_TABLE_ID_2,
49
+ });
50
+ console.log("Creating relationship with:", createRequestBody);
51
+
52
+ const createResponse = await client.createRelationship({
53
+ tableId: QB_TABLE_ID_2,
54
+ body: createRequestBody,
55
+ });
56
+
57
+ console.log(
58
+ "Created relationship response:",
59
+ JSON.stringify(createResponse, null, 2)
60
+ );
61
+
62
+ // Verify creation
63
+ expect(createResponse).toBeDefined();
64
+ expect(createResponse.id).toBeDefined();
65
+ expect(createResponse.parentTableId).toBe(QB_TABLE_ID_1);
66
+ expect(createResponse.childTableId).toBe(QB_TABLE_ID_2);
67
+ expect(createResponse.foreignKeyField).toBeDefined();
68
+ expect(createResponse.foreignKeyField?.label).toBe(uniqueLabel);
69
+
70
+ const relationshipId = createResponse.id;
71
+ const foreignKeyFieldId = createResponse.foreignKeyField?.id;
72
+
73
+ // Step 2: Delete the relationship
74
+ console.log("Deleting relationship with ID:", relationshipId);
75
+ const deleteResponse = await client.deleteRelationship({
76
+ tableId: QB_TABLE_ID_2,
77
+ relationshipId,
78
+ });
79
+
80
+ console.log(
81
+ "Delete relationship response:",
82
+ JSON.stringify(deleteResponse, null, 2)
83
+ );
84
+
85
+ // Verify deletion (adjusted to match actual response)
86
+ expect(deleteResponse).toBeDefined();
87
+ expect(deleteResponse.relationshipId).toBe(relationshipId); // Updated to use "relationshipId"
88
+
89
+ // Step 3: Confirm relationship is gone (or non-functional)
90
+ try {
91
+ const relationships = await client.getRelationships({
92
+ tableId: QB_TABLE_ID_2,
93
+ });
94
+ console.log(
95
+ "Relationships after deletion:",
96
+ JSON.stringify(relationships, null, 2)
97
+ );
98
+ // Check if the relationship is still listed (metadata might persist)
99
+ const deletedRelationship = relationships.relationships.find(
100
+ (r) => r.id === relationshipId
101
+ );
102
+ if (deletedRelationship) {
103
+ console.log("Relationship metadata persists:", deletedRelationship);
104
+ expect(deletedRelationship.foreignKeyField).toBeUndefined(); // Should lack fields
105
+ }
106
+ } catch (error) {
107
+ console.error("Error fetching relationships post-deletion:", error);
108
+ expect(error.message).toMatch(/404|not found/i); // Optional stricter check
109
+ }
110
+
111
+ // Cleanup remaining fields
112
+ const lookupFieldId = createResponse.lookupFields[0]?.id;
113
+ const summaryFieldId = createResponse.summaryFields[0]?.id;
114
+
115
+ if (foreignKeyFieldId) {
116
+ await client.deleteFields({
117
+ tableId: QB_TABLE_ID_2,
118
+ body: { fieldIds: [foreignKeyFieldId] },
119
+ });
120
+ console.log(
121
+ `Deleted foreign key field ${foreignKeyFieldId} from ${QB_TABLE_ID_2}`
122
+ );
123
+ }
124
+ if (lookupFieldId) {
125
+ await client.deleteFields({
126
+ tableId: QB_TABLE_ID_2,
127
+ body: { fieldIds: [lookupFieldId] },
128
+ });
129
+ console.log(
130
+ `Deleted lookup field ${lookupFieldId} from ${QB_TABLE_ID_2}`
131
+ );
132
+ }
133
+ if (summaryFieldId) {
134
+ await client.deleteFields({
135
+ tableId: QB_TABLE_ID_1,
136
+ body: { fieldIds: [summaryFieldId] },
137
+ });
138
+ console.log(
139
+ `Deleted summary field ${summaryFieldId} from ${QB_TABLE_ID_1}`
140
+ );
141
+ }
142
+ }
143
+ );
@@ -0,0 +1,250 @@
1
+ // tests/vitest/qb/tables/updateRelationship.test.ts
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ import {
5
+ createClient,
6
+ QB_REALM,
7
+ QB_USER_TOKEN,
8
+ QB_TABLE_ID_1,
9
+ QB_TABLE_ID_2,
10
+ } from "@tests/setup.ts";
11
+ import {
12
+ UpdateRelationshipRequest,
13
+ CreateFieldRequest,
14
+ } from "@/generated/models";
15
+
16
+ describe("QuickbaseClient Integration - updateRelationship", () => {
17
+ let client: ReturnType<typeof createClient>;
18
+ let relationshipId: number;
19
+ let newLookupFieldId: number | undefined;
20
+ let newChildFieldId: number | undefined;
21
+ let generatedLookupFieldId: number | undefined;
22
+ let generatedSummaryFieldId: number | undefined;
23
+
24
+ // No beforeAll/afterAll since we'll create and delete within the test
25
+
26
+ it("creates, updates, deletes, and verifies a relationship", async () => {
27
+ // Validate environment variables
28
+ if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
29
+ if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
30
+ if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
31
+ if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
32
+
33
+ // Initialize client
34
+ client = createClient(fetch, {
35
+ realm: QB_REALM,
36
+ userToken: QB_USER_TOKEN,
37
+ debug: true,
38
+ });
39
+
40
+ // Step 1: Create fields for the relationship update
41
+ const lookupFieldRequest: CreateFieldRequest = {
42
+ label: `Test Lookup Field_${Date.now()}`,
43
+ fieldType: "text",
44
+ };
45
+ const lookupResponse = await client.createField({
46
+ tableId: QB_TABLE_ID_1,
47
+ body: lookupFieldRequest,
48
+ });
49
+ newLookupFieldId = lookupResponse.id;
50
+ if (!newLookupFieldId) {
51
+ throw new Error("Failed to retrieve ID for newly created lookup field");
52
+ }
53
+ console.log(
54
+ `Created lookup field with ID ${newLookupFieldId} in parent table ${QB_TABLE_ID_1}`
55
+ );
56
+
57
+ const childFieldRequest: CreateFieldRequest = {
58
+ label: `Test Child Numeric_${Date.now()}`,
59
+ fieldType: "numeric",
60
+ };
61
+ const childResponse = await client.createField({
62
+ tableId: QB_TABLE_ID_2,
63
+ body: childFieldRequest,
64
+ });
65
+ newChildFieldId = childResponse.id;
66
+ if (!newChildFieldId) {
67
+ throw new Error("Failed to retrieve ID for newly created child field");
68
+ }
69
+ console.log(
70
+ `Created child field with ID ${newChildFieldId} in child table ${QB_TABLE_ID_2}`
71
+ );
72
+
73
+ // Step 2: Create a new relationship
74
+ const uniqueLabel = `TestRelationship_${Date.now()}`;
75
+ const createRequestBody = {
76
+ parentTableId: QB_TABLE_ID_1,
77
+ foreignKeyField: { label: uniqueLabel },
78
+ lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
79
+ summaryFields: [
80
+ {
81
+ summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
82
+ label: `Sum_${uniqueLabel}`,
83
+ accumulationType: "SUM",
84
+ },
85
+ ],
86
+ };
87
+
88
+ console.log("Creating relationship with:", createRequestBody);
89
+ const createResponse = await client.createRelationship({
90
+ tableId: QB_TABLE_ID_2,
91
+ body: createRequestBody,
92
+ });
93
+ console.log(
94
+ "Created relationship response:",
95
+ JSON.stringify(createResponse, null, 2)
96
+ );
97
+
98
+ expect(createResponse).toBeDefined();
99
+ expect(createResponse.id).toBeDefined();
100
+ expect(createResponse.parentTableId).toBe(QB_TABLE_ID_1);
101
+ expect(createResponse.childTableId).toBe(QB_TABLE_ID_2);
102
+ expect(createResponse.foreignKeyField?.label).toBe(uniqueLabel);
103
+
104
+ relationshipId = createResponse.id;
105
+
106
+ // Step 3: Update the relationship - Add a lookup field
107
+ const tableId = QB_TABLE_ID_2;
108
+ const initialResponse = await client.getRelationships({ tableId });
109
+ const initialRelationship = initialResponse.relationships.find(
110
+ (r) => r.id === relationshipId
111
+ );
112
+ const currentLookupFields = (initialRelationship?.lookupFields || []).map(
113
+ (f) => f.id
114
+ );
115
+ console.log("Current lookupFields before update:", currentLookupFields);
116
+
117
+ const updateLookupRequest: UpdateRelationshipRequest = {
118
+ lookupFieldIds: [newLookupFieldId!],
119
+ };
120
+ console.log(
121
+ "Adding lookup field request:",
122
+ JSON.stringify(updateLookupRequest, null, 2)
123
+ );
124
+
125
+ const updateLookupResponse = await client.updateRelationship({
126
+ tableId,
127
+ relationshipId,
128
+ body: updateLookupRequest,
129
+ });
130
+ console.log(
131
+ "Update lookup response:",
132
+ JSON.stringify(updateLookupResponse, null, 2)
133
+ );
134
+
135
+ const newLookup = updateLookupResponse.lookupFields.find(
136
+ (f) =>
137
+ f.label.includes("Test Lookup Field") &&
138
+ !currentLookupFields.includes(f.id)
139
+ );
140
+ generatedLookupFieldId = newLookup?.id;
141
+ console.log(
142
+ `Captured generated lookup field ID: ${generatedLookupFieldId}`
143
+ );
144
+
145
+ expect(updateLookupResponse.id).toBe(relationshipId);
146
+ expect(updateLookupResponse.childTableId).toBe(QB_TABLE_ID_2);
147
+ expect(updateLookupResponse.parentTableId).toBe(QB_TABLE_ID_1);
148
+ expect(
149
+ updateLookupResponse.lookupFields.some((f) =>
150
+ f.label.includes("Test Lookup Field")
151
+ )
152
+ ).toBe(true);
153
+
154
+ // Step 4: Update the relationship - Add a summary field
155
+ const currentSummaryFields = (updateLookupResponse.summaryFields || []).map(
156
+ (f) => f.id
157
+ );
158
+ console.log("Current summaryFields before update:", currentSummaryFields);
159
+
160
+ const updateSummaryRequest: UpdateRelationshipRequest = {
161
+ summaryFields: [
162
+ {
163
+ summaryFid: newChildFieldId!,
164
+ label: "Test Summary Field",
165
+ accumulationType: "SUM",
166
+ },
167
+ ],
168
+ };
169
+ console.log(
170
+ "Adding summary field request:",
171
+ JSON.stringify(updateSummaryRequest, null, 2)
172
+ );
173
+
174
+ const updateSummaryResponse = await client.updateRelationship({
175
+ tableId,
176
+ relationshipId,
177
+ body: updateSummaryRequest,
178
+ });
179
+ console.log(
180
+ "Update summary response:",
181
+ JSON.stringify(updateSummaryResponse, null, 2)
182
+ );
183
+
184
+ const newSummary = updateSummaryResponse.summaryFields.find(
185
+ (f) =>
186
+ f.label.includes("Test Summary Field") &&
187
+ !currentSummaryFields.includes(f.id)
188
+ );
189
+ generatedSummaryFieldId = newSummary?.id;
190
+ console.log(
191
+ `Captured generated summary field ID: ${generatedSummaryFieldId}`
192
+ );
193
+
194
+ expect(updateSummaryResponse.id).toBe(relationshipId);
195
+ expect(updateSummaryResponse.childTableId).toBe(QB_TABLE_ID_2);
196
+ expect(updateSummaryResponse.parentTableId).toBe(QB_TABLE_ID_1);
197
+ expect(
198
+ updateSummaryResponse.summaryFields.some((f) =>
199
+ f.label.includes("Test Summary Field")
200
+ )
201
+ ).toBe(true);
202
+
203
+ // Step 5: Delete the relationship
204
+ console.log("Deleting relationship with ID:", relationshipId);
205
+ const deleteResponse = await client.deleteRelationship({
206
+ tableId: QB_TABLE_ID_2,
207
+ relationshipId,
208
+ });
209
+ console.log(
210
+ "Delete relationship response:",
211
+ JSON.stringify(deleteResponse, null, 2)
212
+ );
213
+
214
+ expect(deleteResponse).toBeDefined();
215
+ expect(deleteResponse.relationshipId).toBe(relationshipId);
216
+
217
+ // Step 6: Verify deletion
218
+ const relationships = await client.getRelationships({
219
+ tableId: QB_TABLE_ID_2,
220
+ });
221
+ console.log(
222
+ "Relationships after deletion:",
223
+ JSON.stringify(relationships, null, 2)
224
+ );
225
+ const deletedRelationship = relationships.relationships.find(
226
+ (r) => r.id === relationshipId
227
+ );
228
+ expect(deletedRelationship).toBeUndefined(); // Confirm relationship is gone
229
+
230
+ // Cleanup remaining fields from setup
231
+ if (newLookupFieldId) {
232
+ await client.deleteFields({
233
+ tableId: QB_TABLE_ID_1,
234
+ body: { fieldIds: [newLookupFieldId] },
235
+ });
236
+ console.log(
237
+ `Deleted lookup field ${newLookupFieldId} from ${QB_TABLE_ID_1}`
238
+ );
239
+ }
240
+ if (newChildFieldId) {
241
+ await client.deleteFields({
242
+ tableId: QB_TABLE_ID_2,
243
+ body: { fieldIds: [newChildFieldId] },
244
+ });
245
+ console.log(
246
+ `Deleted child field ${newChildFieldId} from ${QB_TABLE_ID_2}`
247
+ );
248
+ }
249
+ }, 60000);
250
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { ThrottleBucket } from "@/ThrottleBucket"; // Adjust path as needed
3
+
4
+ describe("ThrottleBucket Unit Tests", () => {
5
+ it("allows burst capacity immediately", async () => {
6
+ const bucket = new ThrottleBucket(5, 3); // rate: 5, burst: 3
7
+ const startTime = Date.now();
8
+
9
+ await Promise.all([bucket.acquire(), bucket.acquire(), bucket.acquire()]);
10
+
11
+ const duration = Date.now() - startTime;
12
+ expect(duration).toBeLessThan(50); // All 3 should be instant (within 50ms)
13
+ });
14
+
15
+ it("delays calls beyond burst at correct rate", async () => {
16
+ const bucket = new ThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
17
+ const timings: number[] = [];
18
+ const recordTime = async () => {
19
+ await bucket.acquire();
20
+ timings.push(Date.now());
21
+ };
22
+
23
+ const startTime = Date.now();
24
+ await Promise.all([
25
+ recordTime(),
26
+ recordTime(),
27
+ recordTime(),
28
+ recordTime(), // 4th call should delay
29
+ ]);
30
+
31
+ const durations = timings.map((t) => t - startTime);
32
+ expect(durations.length).toBe(4);
33
+ expect(durations[0]).toBeLessThan(50); // 1st call instant
34
+ expect(durations[1]).toBeLessThan(50); // 2nd call instant
35
+ expect(durations[2]).toBeLessThan(50); // 3rd call instant
36
+ expect(durations[3]).toBeGreaterThanOrEqual(180); // 4th call delayed ~200ms
37
+ expect(durations[3]).toBeLessThan(250); // Shouldn’t exceed 250ms
38
+ });
39
+
40
+ it("maintains rate over sustained calls", async () => {
41
+ const bucket = new ThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
42
+ const timings: number[] = [];
43
+ const recordTime = async () => {
44
+ await bucket.acquire();
45
+ timings.push(Date.now());
46
+ };
47
+
48
+ const startTime = Date.now();
49
+ await Promise.all([
50
+ recordTime(),
51
+ recordTime(),
52
+ recordTime(),
53
+ recordTime(),
54
+ recordTime(), // 5 calls total
55
+ ]);
56
+
57
+ const durations = timings.map((t) => t - startTime);
58
+ expect(durations.length).toBe(5);
59
+ expect(durations[0]).toBeLessThan(50); // 1st instant
60
+ expect(durations[1]).toBeLessThan(50); // 2nd instant
61
+ expect(durations[2]).toBeGreaterThanOrEqual(450); // 3rd delayed ~500ms (1/2 sec)
62
+ expect(durations[2]).toBeLessThan(550);
63
+ expect(durations[3]).toBeGreaterThanOrEqual(950); // 4th delayed ~1000ms
64
+ expect(durations[3]).toBeLessThan(1050);
65
+ expect(durations[4]).toBeGreaterThanOrEqual(1450); // 5th delayed ~1500ms
66
+ expect(durations[4]).toBeLessThan(1550);
67
+ });
68
+
69
+ it("handles high concurrency without exceeding burst", async () => {
70
+ const bucket = new ThrottleBucket(5, 3); // rate: 5, burst: 3
71
+ const timings: number[] = [];
72
+ const recordTime = async () => {
73
+ await bucket.acquire();
74
+ timings.push(Date.now());
75
+ };
76
+
77
+ const startTime = Date.now();
78
+ await Promise.all(
79
+ Array(10)
80
+ .fill(null)
81
+ .map(() => recordTime())
82
+ ); // 10 concurrent calls
83
+
84
+ const durations = timings.map((t) => t - startTime);
85
+ expect(durations.length).toBe(10);
86
+ // First 3 should be instant
87
+ expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true);
88
+ // Next calls should be staggered at ~200ms intervals
89
+ for (let i = 3; i < 10; i++) {
90
+ const expectedDelay = (i - 2) * 200; // After burst, 200ms per token
91
+ expect(durations[i]).toBeGreaterThanOrEqual(expectedDelay - 50);
92
+ expect(durations[i]).toBeLessThan(expectedDelay + 50);
93
+ }
94
+ });
95
+ });