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.
- package/CHANGELOG.md +29 -0
- package/README.md +46 -130
- package/dist/esm/quickbase.d.ts +822 -119
- package/dist/esm/quickbase.js +373 -236
- package/dist/esm/quickbase.js.map +1 -1
- package/dist/esm/quickbase.min.js +1 -1
- package/dist/esm/quickbase.min.js.map +1 -1
- package/dist/umd/quickbase.umd.js +373 -236
- package/dist/umd/quickbase.umd.js.map +1 -1
- package/dist/umd/quickbase.umd.min.js +1 -1
- package/dist/umd/quickbase.umd.min.js.map +1 -1
- package/open-api/generate-unified-interface.ts +97 -17
- package/open-api/utils/generateJsDoc.ts +96 -0
- package/package.json +3 -1
- package/src/ThrottleBucket.ts +17 -12
- package/src/authorizationStrategy.ts +246 -0
- package/src/generated-unified/QuickbaseClient.ts +809 -119
- package/src/invokeMethod.ts +150 -265
- package/src/quickbaseClient.ts +85 -58
- package/src/rateLimiter.ts +25 -0
- package/src/tokenCache.ts +10 -26
- package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
- package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
- package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
- package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
- package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
- package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
- package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
- package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
- package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
- package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
- package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
- package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
- package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
- package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
- package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
- package/tests/vitest/unit/fields/createField.test.ts +180 -116
- package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
- package/tests/vitest/unit/fields/updateField.test.ts +12 -7
- package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
- package/tests/vitest/unit/index.ts +2 -1
- package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
- package/tests/vitest/unit/rateLimit.test.ts +4 -2
- package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
- package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
- package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
- package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
- package/tools/log-gen.ts +14 -5
- package/tools/log-gen.yaml +710 -268
- 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/
|
|
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
|
|
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
|
+
});
|