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
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import {
3
+ createClient,
4
+ mockFetch,
5
+ QB_REALM,
6
+ QB_USER_TOKEN,
7
+ QB_TABLE_ID_1,
8
+ } from "@tests/setup.ts";
9
+ import { RunFormulaRequest, RunFormula200Response } from "@/generated/models";
10
+
11
+ describe("QuickbaseClient Unit - runFormula", () => {
12
+ let client: ReturnType<typeof createClient>;
13
+
14
+ beforeEach(() => {
15
+ mockFetch.mockClear();
16
+ });
17
+
18
+ it("initializes without errors and has runFormula method", () => {
19
+ client = createClient(mockFetch, { debug: true });
20
+ expect(client).toBeDefined();
21
+ expect(typeof client.runFormula).toBe("function");
22
+ });
23
+
24
+ it("calls runFormula successfully with user token", async () => {
25
+ client = createClient(mockFetch, { debug: true });
26
+
27
+ const request: RunFormulaRequest = {
28
+ from: QB_TABLE_ID_1,
29
+ formula: "Sum([NumericField],20)",
30
+ rid: 1,
31
+ };
32
+
33
+ const mockResponse: RunFormula200Response = {
34
+ result: "30",
35
+ };
36
+
37
+ mockFetch.mockResolvedValueOnce({
38
+ ok: true,
39
+ status: 200,
40
+ json: () => Promise.resolve(mockResponse),
41
+ });
42
+
43
+ const result = await client.runFormula({ body: request });
44
+
45
+ expect(result).toEqual(mockResponse);
46
+ expect(mockFetch).toHaveBeenCalledWith(
47
+ "https://api.quickbase.com/v1/formula/run",
48
+ expect.objectContaining({
49
+ method: "POST",
50
+ headers: expect.objectContaining({
51
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
52
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
53
+ "Content-Type": "application/json",
54
+ }),
55
+ // Check body as parsed object to ignore order
56
+ body: expect.any(String), // First ensure it’s a string
57
+ })
58
+ );
59
+
60
+ // Additional check for body content
61
+ const calls = mockFetch.mock.calls;
62
+ expect(calls.length).toBe(1);
63
+ const fetchBody = calls[0][1].body as string;
64
+ expect(JSON.parse(fetchBody)).toEqual(request);
65
+
66
+ expect(result.result).toBe("30");
67
+ });
68
+
69
+ it("handles missing rid for formulas not requiring it", async () => {
70
+ client = createClient(mockFetch, { debug: true });
71
+
72
+ const request: RunFormulaRequest = {
73
+ from: QB_TABLE_ID_1,
74
+ formula: "User()",
75
+ // rid omitted intentionally
76
+ };
77
+
78
+ const mockResponse: RunFormula200Response = {
79
+ result: "user@example.com",
80
+ };
81
+
82
+ mockFetch.mockResolvedValueOnce({
83
+ ok: true,
84
+ status: 200,
85
+ json: () => Promise.resolve(mockResponse),
86
+ });
87
+
88
+ const result = await client.runFormula({ body: request });
89
+
90
+ expect(result).toEqual(mockResponse);
91
+ expect(mockFetch).toHaveBeenCalledWith(
92
+ "https://api.quickbase.com/v1/formula/run",
93
+ expect.objectContaining({
94
+ method: "POST",
95
+ headers: expect.objectContaining({
96
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
97
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
98
+ "Content-Type": "application/json",
99
+ }),
100
+ body: expect.any(String),
101
+ })
102
+ );
103
+
104
+ const calls = mockFetch.mock.calls;
105
+ expect(calls.length).toBe(1);
106
+ const fetchBody = calls[0][1].body as string;
107
+ expect(JSON.parse(fetchBody)).toEqual(request);
108
+
109
+ expect(result.result).toBe("user@example.com");
110
+ });
111
+
112
+ it("handles API error for invalid formula", async () => {
113
+ client = createClient(mockFetch, { debug: true });
114
+
115
+ const request: RunFormulaRequest = {
116
+ from: QB_TABLE_ID_1,
117
+ formula: "InvalidFormula()",
118
+ rid: 1,
119
+ };
120
+
121
+ mockFetch.mockResolvedValueOnce({
122
+ ok: false,
123
+ status: 400,
124
+ json: () => Promise.resolve({ message: "Invalid formula syntax" }),
125
+ });
126
+
127
+ await expect(client.runFormula({ body: request })).rejects.toThrow(
128
+ "API Error: Invalid formula syntax (Status: 400)"
129
+ );
130
+
131
+ expect(mockFetch).toHaveBeenCalledWith(
132
+ "https://api.quickbase.com/v1/formula/run",
133
+ expect.objectContaining({
134
+ method: "POST",
135
+ headers: expect.objectContaining({
136
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
137
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
138
+ "Content-Type": "application/json",
139
+ }),
140
+ body: expect.any(String),
141
+ })
142
+ );
143
+
144
+ const calls = mockFetch.mock.calls;
145
+ expect(calls.length).toBe(1);
146
+ const fetchBody = calls[0][1].body as string;
147
+ expect(JSON.parse(fetchBody)).toEqual(request);
148
+ });
149
+
150
+ it("handles temp token with retry on 401", async () => {
151
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
152
+
153
+ const request: RunFormulaRequest = {
154
+ from: QB_TABLE_ID_1,
155
+ formula: "Sum([NumericField],20)",
156
+ rid: 1,
157
+ };
158
+
159
+ const mockResponse: RunFormula200Response = {
160
+ result: "30",
161
+ };
162
+
163
+ mockFetch
164
+ .mockResolvedValueOnce({
165
+ ok: true,
166
+ status: 200,
167
+ json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
168
+ })
169
+ .mockResolvedValueOnce({
170
+ ok: false,
171
+ status: 401,
172
+ json: () => Promise.resolve({ message: "Unauthorized" }),
173
+ })
174
+ .mockResolvedValueOnce({
175
+ ok: true,
176
+ status: 200,
177
+ json: () =>
178
+ Promise.resolve({ temporaryAuthorization: "new_temp_token" }),
179
+ })
180
+ .mockResolvedValueOnce({
181
+ ok: true,
182
+ status: 200,
183
+ json: () => Promise.resolve(mockResponse),
184
+ });
185
+
186
+ const result = await client.runFormula({ body: request });
187
+
188
+ expect(result).toEqual(mockResponse);
189
+ expect(mockFetch).toHaveBeenCalledTimes(4);
190
+ expect(mockFetch).toHaveBeenNthCalledWith(
191
+ 1,
192
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
193
+ expect.any(Object)
194
+ );
195
+ expect(mockFetch).toHaveBeenNthCalledWith(
196
+ 4,
197
+ "https://api.quickbase.com/v1/formula/run",
198
+ expect.objectContaining({
199
+ headers: expect.objectContaining({
200
+ Authorization: "QB-TEMP-TOKEN new_temp_token",
201
+ }),
202
+ })
203
+ );
204
+ });
205
+ });
@@ -9,7 +9,7 @@ export * from "./apps/getApp.test.ts";
9
9
  export * from "./auth/fetchTempToken401.test.ts";
10
10
  export * from "./auth/getTempToken.test.ts";
11
11
  export * from "./auth/retryOn401.test.ts";
12
- export * from "./auth/two401s.test.ts";
12
+ export * from "./auth/retryTempTokenTwo401.test.ts";
13
13
 
14
14
  // Fields
15
15
  export * from "./fields/getFields.test.ts";
@@ -31,3 +31,4 @@ export * from "./tables/getAppTables.test.ts";
31
31
  export * from "./tables/getRelationships.test.ts";
32
32
  export * from "./tables/getTable.test.ts";
33
33
  export * from "./tables/updateTable.test.ts";
34
+ export * from "./tables/createRelationship.test.ts";
@@ -0,0 +1,131 @@
1
+ // tests/vitest/unit/inferHttpMethod.test.ts
2
+
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
+ import { quickbase } from "../../../src/quickbaseClient";
5
+ import { mockFetch, QB_APP_ID, QB_REALM } from "../../setup.ts";
6
+ import {
7
+ UpdateAppRequest,
8
+ UpdateApp200Response,
9
+ } from "../../../src/generated/models";
10
+ import { ChangesetSolution200Response } from "../../../src/generated/models";
11
+
12
+ describe("QuickbaseClient Unit - inferHttpMethod", () => {
13
+ let clientTemp: ReturnType<typeof quickbase>;
14
+ let clientUser: ReturnType<typeof quickbase>;
15
+
16
+ beforeEach(() => {
17
+ mockFetch.mockClear();
18
+ vi.useFakeTimers();
19
+
20
+ clientTemp = quickbase({
21
+ realm: QB_REALM,
22
+ userToken: "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
23
+ debug: false,
24
+ fetchApi: mockFetch,
25
+ throttle: { rate: 10, burst: 10 },
26
+ useTempTokens: true,
27
+ tempTokenLifespan: 500,
28
+ });
29
+
30
+ clientUser = quickbase({
31
+ realm: QB_REALM,
32
+ userToken: "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
33
+ debug: false,
34
+ fetchApi: mockFetch,
35
+ throttle: { rate: 10, burst: 10 },
36
+ useTempTokens: false,
37
+ });
38
+ });
39
+
40
+ afterEach(() => {
41
+ vi.useRealTimers();
42
+ mockFetch.mockReset();
43
+ });
44
+
45
+ const mockUpdateAppResponse: UpdateApp200Response = {
46
+ id: QB_APP_ID,
47
+ name: "Test App",
48
+ description: "",
49
+ created: new Date("2020-01-01T00:00:00Z"),
50
+ updated: new Date("2020-01-01T00:00:00Z"),
51
+ dateFormat: "MM-DD-YYYY",
52
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
53
+ hasEveryoneOnTheInternet: false,
54
+ ancestorId: undefined,
55
+ variables: [],
56
+ securityProperties: {},
57
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
58
+ dataClassification: "None",
59
+ };
60
+
61
+ const mockChangesetResponse: ChangesetSolution200Response = [{ changes: [] }];
62
+
63
+ const setupMockFetch = () => {
64
+ mockFetch.mockReset();
65
+ mockFetch
66
+ .mockResolvedValueOnce({
67
+ ok: true,
68
+ status: 200,
69
+ json: () => Promise.resolve({ temporaryAuthorization: "token1" }),
70
+ })
71
+ .mockResolvedValueOnce({
72
+ ok: true,
73
+ status: 200,
74
+ json: () => Promise.resolve(mockUpdateAppResponse),
75
+ });
76
+ };
77
+
78
+ const setupMockFetchForChangeset = () => {
79
+ mockFetch.mockReset();
80
+ mockFetch.mockResolvedValueOnce({
81
+ ok: true,
82
+ status: 200,
83
+ json: () => Promise.resolve(mockChangesetResponse),
84
+ });
85
+ };
86
+
87
+ it("correctly identifies POST for updateApp", async () => {
88
+ setupMockFetch();
89
+ await clientTemp.updateApp({
90
+ appId: QB_APP_ID,
91
+ body: { name: "Test App" },
92
+ });
93
+ const call = mockFetch.mock.calls[1];
94
+ expect(call[1].method).toBe("POST");
95
+ });
96
+
97
+ it("correctly identifies POST for runReport", async () => {
98
+ setupMockFetch();
99
+ await clientTemp.runReport({
100
+ tableId: QB_APP_ID,
101
+ reportId: "report1",
102
+ body: { skip: 0, top: 10 },
103
+ });
104
+ const call = mockFetch.mock.calls[1];
105
+ expect(call[1].method).toBe("POST");
106
+ });
107
+
108
+ it("correctly identifies PUT for changesetSolution", async () => {
109
+ setupMockFetchForChangeset();
110
+ await clientUser.changesetSolution({
111
+ solutionId: "solution1",
112
+ body: "name: Test Solution\nversion: 1.0",
113
+ });
114
+ const call = mockFetch.mock.calls[0];
115
+ expect(call[1].method).toBe("PUT");
116
+ });
117
+
118
+ it("correctly identifies GET for getTable", async () => {
119
+ setupMockFetch();
120
+ await clientTemp.getTable({ appId: QB_APP_ID, tableId: "table1" });
121
+ const call = mockFetch.mock.calls[1];
122
+ expect(call[1].method).toBe("GET");
123
+ });
124
+
125
+ it("correctly identifies DELETE for deleteTable", async () => {
126
+ setupMockFetch();
127
+ await clientTemp.deleteTable({ appId: QB_APP_ID, tableId: "table1" });
128
+ const call = mockFetch.mock.calls[1];
129
+ expect(call[1].method).toBe("DELETE");
130
+ });
131
+ });
@@ -140,6 +140,7 @@ describe("QuickbaseClient Unit - Rate Limit Handling", () => {
140
140
  userToken: QB_USER_TOKEN,
141
141
  debug: true,
142
142
  throttle: { rate: 5, burst: 5 },
143
+ maxRetries: 0, // Prevent retries for this test
143
144
  });
144
145
 
145
146
  const appId = QB_APP_ID;
@@ -177,13 +178,14 @@ describe("QuickbaseClient Unit - Rate Limit Handling", () => {
177
178
  client.getApp({ appId }),
178
179
  client.getApp({ appId }),
179
180
  client.getApp({ appId }),
180
- client.getApp({ appId }), // 6th call triggers throttle
181
+ client.getApp({ appId }), // 6th call should be throttled
181
182
  ]);
182
183
  const endTime = Date.now();
183
184
  const durationMs = endTime - startTime;
184
185
 
186
+ console.log(`[Test Debug] mockFetch calls: ${mockFetch.mock.calls.length}`);
185
187
  expect(mockFetch).toHaveBeenCalledTimes(6);
186
- expect(durationMs).toBeGreaterThanOrEqual(200);
188
+ expect(durationMs).toBeGreaterThanOrEqual(200); // 1/5 sec for 6th call
187
189
  expect(durationMs).toBeLessThan(1000);
188
190
  });
189
191
  });
@@ -0,0 +1,212 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ createClient,
4
+ mockFetch,
5
+ QB_TABLE_ID_1,
6
+ QB_TABLE_ID_2,
7
+ } from "@tests/setup.ts";
8
+ import {
9
+ CreateRelationship200Response,
10
+ CreateRelationshipRequest,
11
+ } from "/home/drew/Projects/quickbase-js/src/generated/models";
12
+
13
+ describe("QuickbaseClient Unit - createRelationship", () => {
14
+ let client: ReturnType<typeof createClient>;
15
+
16
+ beforeEach(() => {
17
+ mockFetch.mockClear();
18
+ client = createClient(mockFetch, { debug: true });
19
+ });
20
+
21
+ it("initializes without errors", () => {
22
+ expect(client).toBeDefined();
23
+ });
24
+
25
+ it("has createRelationship method", () => {
26
+ expect(typeof client.createRelationship).toBe("function");
27
+ });
28
+
29
+ it("sends correct request and handles successful response", async () => {
30
+ const mockResponse: CreateRelationship200Response = {
31
+ id: 6,
32
+ foreignKeyField: {
33
+ id: 6,
34
+ label: "my relationship field",
35
+ type: "numeric",
36
+ },
37
+ lookupFields: [
38
+ { id: 7, label: "Field 1", type: "text" },
39
+ { id: 8, label: "Field 2", type: "text" },
40
+ { id: 9, label: "Field 3", type: "text" },
41
+ ],
42
+ isCrossApp: false,
43
+ parentTableId: QB_TABLE_ID_1,
44
+ childTableId: QB_TABLE_ID_2,
45
+ summaryFields: [
46
+ { id: 10, label: "my first summary field", type: "numeric" },
47
+ { id: 11, label: "my second summary field", type: "numeric" },
48
+ ],
49
+ };
50
+
51
+ mockFetch.mockResolvedValueOnce({
52
+ ok: true,
53
+ status: 200,
54
+ json: () => Promise.resolve(mockResponse),
55
+ });
56
+
57
+ const requestBody: CreateRelationshipRequest = {
58
+ parentTableId: QB_TABLE_ID_1,
59
+ foreignKeyField: { label: "my relationship field" },
60
+ lookupFieldIds: [1, 2, 3],
61
+ summaryFields: [
62
+ {
63
+ summaryFid: 3,
64
+ label: "my first summary field",
65
+ accumulationType: "AVG",
66
+ where: "{'3'.EX.1}",
67
+ },
68
+ {
69
+ summaryFid: 4,
70
+ label: "my second summary field",
71
+ accumulationType: "SUM",
72
+ },
73
+ ],
74
+ };
75
+
76
+ const tableId = QB_TABLE_ID_2;
77
+
78
+ const result = await client.createRelationship({
79
+ tableId,
80
+ body: requestBody,
81
+ });
82
+
83
+ expect(mockFetch).toHaveBeenCalledTimes(1);
84
+ expect(mockFetch).toHaveBeenCalledWith(
85
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationship`,
86
+ expect.objectContaining({
87
+ method: "POST",
88
+ headers: expect.objectContaining({
89
+ "Content-Type": "application/json",
90
+ "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
91
+ Authorization:
92
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
93
+ }),
94
+ body: JSON.stringify(requestBody),
95
+ })
96
+ );
97
+ expect(result).toEqual(mockResponse);
98
+ expect(result.id).toBe(6);
99
+ expect(result.parentTableId).toBe(QB_TABLE_ID_1);
100
+ expect(result.childTableId).toBe(QB_TABLE_ID_2);
101
+ expect(result.foreignKeyField).toBeDefined();
102
+ if (result.foreignKeyField) {
103
+ expect(result.foreignKeyField.label).toBe("my relationship field");
104
+ }
105
+ expect(result.lookupFields).toHaveLength(3);
106
+ expect(result.summaryFields).toBeDefined();
107
+ if (result.summaryFields) {
108
+ expect(result.summaryFields).toHaveLength(2);
109
+ expect(result.summaryFields[0].label).toBe("my first summary field");
110
+ expect(result.summaryFields[1].label).toBe("my second summary field");
111
+ }
112
+ });
113
+
114
+ it("handles error from TablesApi", async () => {
115
+ mockFetch.mockResolvedValueOnce({
116
+ ok: false,
117
+ status: 400,
118
+ json: () => Promise.resolve({ message: "Invalid request payload" }),
119
+ });
120
+
121
+ const requestBody: CreateRelationshipRequest = {
122
+ parentTableId: QB_TABLE_ID_1,
123
+ foreignKeyField: { label: "my relationship field" },
124
+ lookupFieldIds: [1, 2, 3],
125
+ summaryFields: [
126
+ { summaryFid: 3, label: "invalid", accumulationType: "INVALID" },
127
+ ],
128
+ };
129
+
130
+ const tableId = QB_TABLE_ID_2;
131
+
132
+ await expect(
133
+ client.createRelationship({
134
+ tableId,
135
+ body: requestBody,
136
+ })
137
+ ).rejects.toThrow("API Error: Invalid request payload (Status: 400)");
138
+
139
+ expect(mockFetch).toHaveBeenCalledTimes(1);
140
+ expect(mockFetch).toHaveBeenCalledWith(
141
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationship`,
142
+ expect.objectContaining({
143
+ method: "POST",
144
+ headers: expect.objectContaining({
145
+ "Content-Type": "application/json",
146
+ "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
147
+ Authorization:
148
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
149
+ }),
150
+ body: JSON.stringify(requestBody),
151
+ })
152
+ );
153
+ });
154
+
155
+ it("handles response with undefined foreignKeyField and summaryFields", async () => {
156
+ const mockResponse: CreateRelationship200Response = {
157
+ id: 6,
158
+ foreignKeyField: undefined,
159
+ lookupFields: [
160
+ { id: 7, label: "Field 1", type: "text" },
161
+ { id: 8, label: "Field 2", type: "text" },
162
+ { id: 9, label: "Field 3", type: "text" },
163
+ ],
164
+ isCrossApp: false,
165
+ parentTableId: QB_TABLE_ID_1,
166
+ childTableId: QB_TABLE_ID_2,
167
+ summaryFields: undefined,
168
+ };
169
+
170
+ mockFetch.mockResolvedValueOnce({
171
+ ok: true,
172
+ status: 200,
173
+ json: () => Promise.resolve(mockResponse),
174
+ });
175
+
176
+ const requestBody: CreateRelationshipRequest = {
177
+ parentTableId: QB_TABLE_ID_1,
178
+ foreignKeyField: undefined,
179
+ lookupFieldIds: [1, 2, 3],
180
+ summaryFields: undefined,
181
+ };
182
+
183
+ const tableId = QB_TABLE_ID_2;
184
+
185
+ const result = await client.createRelationship({
186
+ tableId,
187
+ body: requestBody,
188
+ });
189
+
190
+ expect(mockFetch).toHaveBeenCalledTimes(1);
191
+ expect(mockFetch).toHaveBeenCalledWith(
192
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationship`,
193
+ expect.objectContaining({
194
+ method: "POST",
195
+ headers: expect.objectContaining({
196
+ "Content-Type": "application/json",
197
+ "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
198
+ Authorization:
199
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
200
+ }),
201
+ body: JSON.stringify(requestBody),
202
+ })
203
+ );
204
+ expect(result).toEqual(mockResponse);
205
+ expect(result.id).toBe(6);
206
+ expect(result.parentTableId).toBe(QB_TABLE_ID_1);
207
+ expect(result.childTableId).toBe(QB_TABLE_ID_2);
208
+ expect(result.foreignKeyField).toBeUndefined();
209
+ expect(result.lookupFields).toHaveLength(3);
210
+ expect(result.summaryFields).toBeUndefined();
211
+ });
212
+ });
@@ -0,0 +1,97 @@
1
+ // tests/vitest/unit/tables/deleteRelationship.test.ts
2
+
3
+ import { describe, it, expect, beforeEach } from "vitest";
4
+ import {
5
+ createClient,
6
+ mockFetch,
7
+ QB_TABLE_ID_2, // Child table
8
+ } from "@tests/setup.ts";
9
+
10
+ describe("QuickbaseClient Unit - deleteRelationship", () => {
11
+ let client: ReturnType<typeof createClient>;
12
+
13
+ beforeEach(() => {
14
+ mockFetch.mockClear();
15
+ client = createClient(mockFetch, { debug: true });
16
+ });
17
+
18
+ it("initializes without errors", () => {
19
+ expect(client).toBeDefined();
20
+ });
21
+
22
+ it("has deleteRelationship method", () => {
23
+ expect(typeof client.deleteRelationship).toBe("function");
24
+ });
25
+
26
+ it("sends correct DELETE request and handles successful response", async () => {
27
+ // Mock response for a successful deletion
28
+ const mockResponse = {
29
+ deletedRelationshipId: 6, // Matches the relationshipId
30
+ };
31
+
32
+ mockFetch.mockResolvedValueOnce({
33
+ ok: true,
34
+ status: 200,
35
+ json: () => Promise.resolve(mockResponse),
36
+ });
37
+
38
+ const tableId = QB_TABLE_ID_2; // Child table
39
+ const relationshipId = 6; // Example relationship ID (foreign key field ID)
40
+
41
+ const result = await client.deleteRelationship({
42
+ tableId,
43
+ relationshipId,
44
+ });
45
+
46
+ // Verify the fetch call
47
+ expect(mockFetch).toHaveBeenCalledTimes(1);
48
+ expect(mockFetch).toHaveBeenCalledWith(
49
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationship/6`,
50
+ expect.objectContaining({
51
+ method: "DELETE",
52
+ headers: expect.objectContaining({
53
+ "Content-Type": "application/json",
54
+ "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
55
+ Authorization:
56
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
57
+ }),
58
+ })
59
+ );
60
+
61
+ // Verify the response
62
+ expect(result).toEqual(mockResponse);
63
+ expect(result.deletedRelationshipId).toBe(6);
64
+ });
65
+
66
+ it("handles error when relationship is not found", async () => {
67
+ mockFetch.mockResolvedValueOnce({
68
+ ok: false,
69
+ status: 404,
70
+ json: () => Promise.resolve({ message: "Relationship not found" }),
71
+ });
72
+
73
+ const tableId = QB_TABLE_ID_2;
74
+ const relationshipId = 999; // Non-existent relationship ID
75
+
76
+ await expect(
77
+ client.deleteRelationship({
78
+ tableId,
79
+ relationshipId,
80
+ })
81
+ ).rejects.toThrow("API Error: Relationship not found (Status: 404)");
82
+
83
+ expect(mockFetch).toHaveBeenCalledTimes(1);
84
+ expect(mockFetch).toHaveBeenCalledWith(
85
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_2}/relationship/999`,
86
+ expect.objectContaining({
87
+ method: "DELETE",
88
+ headers: expect.objectContaining({
89
+ "Content-Type": "application/json",
90
+ "QB-Realm-Hostname": "builderprogram-dbradford6815.quickbase.com",
91
+ Authorization:
92
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
93
+ }),
94
+ })
95
+ );
96
+ });
97
+ });