quickbase-js 0.1.0-beta.7 → 0.1.0-beta.8
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 +26 -0
- package/dist/esm/quickbase.d.ts +589 -125
- package/dist/esm/quickbase.js +484 -145
- 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 +492 -144
- 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/fix-spec-main.ts +69 -68
- package/open-api/generate-override-report.ts +214 -0
- package/open-api/generate-unified-interface.ts +73 -94
- package/open-api/output/override-report.html +11 -0
- package/open-api/output/override-report.json +14395 -58
- package/open-api/output/quickbase-fixed.json +583 -2
- package/open-api/schema/tags/fields.ts +331 -16
- package/open-api/schema/tags/index.ts +2 -4
- package/open-api/types/spec.ts +2 -1
- package/package.json +31 -33
- package/src/RateLimitError.ts +12 -0
- package/src/ThrottleBucket.ts +36 -0
- package/src/generated-unified/QuickbaseClient.ts +119 -119
- package/src/invokeMethod.ts +357 -0
- package/src/quickbaseClient.ts +51 -209
- package/tests/setup.ts +2 -0
- package/tests/vitest/qb/apps/getApp.test.ts +6 -2
- package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
- package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
- package/tests/vitest/qb/auth/retryUserToken401.test.ts +56 -0
- package/tests/vitest/qb/fields/createField.test.ts +1 -2
- package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
- package/tests/vitest/qb/fields/updateField.test.ts +93 -0
- package/tests/vitest/qb/index.ts +1 -0
- package/tests/vitest/qb/tables/updateRelationship.test.ts +277 -0
- package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
- package/tests/vitest/unit/apps/updateApp.test.ts +381 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +6 -26
- package/tests/vitest/unit/auth/{retryOn401.test.ts → retryTempToken401.test.ts} +3 -6
- package/tests/vitest/unit/auth/{two401s.test.ts → retryTempTokenTwo401.test.ts} +4 -9
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -0
- package/tests/vitest/unit/fields/createField.test.ts +8 -12
- package/tests/vitest/unit/fields/getFieldUsage.test.ts +33 -14
- package/tests/vitest/unit/fields/updateField.test.ts +280 -0
- package/tests/vitest/unit/rateLimit.test.ts +189 -0
- package/tests/vitest/unit/tables/updateRelationship.test.ts +305 -0
- package/tests/vitest/unit/tokenLifespan.test.ts +138 -0
- package/tools/log-gen.ts +8 -4
- package/tools/log-gen.yaml +3004 -1848
- package/tsconfig.json +5 -2
- package/vitest.config.ts +2 -2
- package/notes.txt +0 -158
- package/tools/generate-types.ts +0 -29
- package/tools/openapi-schema-validator.ts +0 -38
- package/tools/spec-analyzer.ts +0 -163
- package/tools/split-spec.ts +0 -148
- package/tools/test-import.ts +0 -3
- package/tools/validate-spec.ts +0 -40
- /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
- /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// tests/vitest/unit/rateLimit.test.ts
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createClient,
|
|
5
|
+
mockFetch,
|
|
6
|
+
QB_APP_ID,
|
|
7
|
+
QB_REALM,
|
|
8
|
+
QB_USER_TOKEN,
|
|
9
|
+
} from "@tests/setup.ts";
|
|
10
|
+
import { RateLimitError } from "@/RateLimitError";
|
|
11
|
+
import { GetApp200Response } from "@/generated/models";
|
|
12
|
+
|
|
13
|
+
describe("QuickbaseClient Unit - Rate Limit Handling", () => {
|
|
14
|
+
let client: ReturnType<typeof createClient>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockFetch.mockClear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("succeeds after retrying on 429 Too Many Requests", async () => {
|
|
21
|
+
client = createClient(mockFetch, {
|
|
22
|
+
realm: QB_REALM,
|
|
23
|
+
userToken: QB_USER_TOKEN,
|
|
24
|
+
debug: true,
|
|
25
|
+
maxRetries: 2,
|
|
26
|
+
retryDelay: 100,
|
|
27
|
+
throttle: { rate: 10, burst: 10 },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const appId = QB_APP_ID;
|
|
31
|
+
|
|
32
|
+
const mockResponse: GetApp200Response = {
|
|
33
|
+
id: appId,
|
|
34
|
+
name: "TestApp",
|
|
35
|
+
created: "2025-01-01T00:00:00Z",
|
|
36
|
+
updated: "2025-01-02T00:00:00Z",
|
|
37
|
+
description: "Test app",
|
|
38
|
+
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
39
|
+
dateFormat: "MM-DD-YYYY",
|
|
40
|
+
hasEveryoneOnTheInternet: false,
|
|
41
|
+
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
42
|
+
securityProperties: {
|
|
43
|
+
allowClone: false,
|
|
44
|
+
allowExport: true,
|
|
45
|
+
enableAppTokens: true,
|
|
46
|
+
hideFromPublic: false,
|
|
47
|
+
mustBeRealmApproved: false,
|
|
48
|
+
useIPFilter: false,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
mockFetch
|
|
53
|
+
.mockResolvedValueOnce({
|
|
54
|
+
ok: false,
|
|
55
|
+
status: 429,
|
|
56
|
+
headers: new Headers({ "Retry-After": "1" }),
|
|
57
|
+
json: () => Promise.resolve({ message: "Too Many Requests" }),
|
|
58
|
+
})
|
|
59
|
+
.mockResolvedValueOnce({
|
|
60
|
+
ok: true,
|
|
61
|
+
status: 200,
|
|
62
|
+
json: () => Promise.resolve(mockResponse),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await client.getApp({ appId });
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual({
|
|
68
|
+
...mockResponse,
|
|
69
|
+
created: new Date("2025-01-01T00:00:00Z"),
|
|
70
|
+
updated: new Date("2025-01-02T00:00:00Z"),
|
|
71
|
+
});
|
|
72
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
73
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
74
|
+
1,
|
|
75
|
+
`https://api.quickbase.com/v1/apps/${appId}`,
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
method: "GET",
|
|
78
|
+
headers: expect.objectContaining({
|
|
79
|
+
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
80
|
+
Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
}),
|
|
83
|
+
credentials: "omit",
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
87
|
+
2,
|
|
88
|
+
`https://api.quickbase.com/v1/apps/${appId}`,
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
method: "GET",
|
|
91
|
+
headers: expect.objectContaining({
|
|
92
|
+
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
93
|
+
Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
}),
|
|
96
|
+
credentials: "omit",
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("throws RateLimitError after max retries on 429", async () => {
|
|
102
|
+
client = createClient(mockFetch, {
|
|
103
|
+
realm: QB_REALM,
|
|
104
|
+
userToken: QB_USER_TOKEN,
|
|
105
|
+
debug: true,
|
|
106
|
+
maxRetries: 1,
|
|
107
|
+
retryDelay: 100,
|
|
108
|
+
throttle: { rate: 10, burst: 10 },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const appId = QB_APP_ID;
|
|
112
|
+
|
|
113
|
+
mockFetch
|
|
114
|
+
.mockResolvedValueOnce({
|
|
115
|
+
ok: false,
|
|
116
|
+
status: 429,
|
|
117
|
+
headers: new Headers({ "Retry-After": "1" }),
|
|
118
|
+
json: () => Promise.resolve({ message: "Too Many Requests" }),
|
|
119
|
+
})
|
|
120
|
+
.mockResolvedValueOnce({
|
|
121
|
+
ok: false,
|
|
122
|
+
status: 429,
|
|
123
|
+
headers: new Headers({ "Retry-After": "1" }),
|
|
124
|
+
json: () => Promise.resolve({ message: "Too Many Requests" }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await expect(client.getApp({ appId })).rejects.toMatchObject({
|
|
128
|
+
name: "RateLimitError",
|
|
129
|
+
message: "API Error: Too Many Requests (Status: 429)",
|
|
130
|
+
status: 429,
|
|
131
|
+
retryAfter: 1,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("respects throttle limits without exceeding rate", async () => {
|
|
138
|
+
client = createClient(mockFetch, {
|
|
139
|
+
realm: QB_REALM,
|
|
140
|
+
userToken: QB_USER_TOKEN,
|
|
141
|
+
debug: true,
|
|
142
|
+
throttle: { rate: 5, burst: 5 },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const appId = QB_APP_ID;
|
|
146
|
+
|
|
147
|
+
const mockResponse: GetApp200Response = {
|
|
148
|
+
id: appId,
|
|
149
|
+
name: "TestApp",
|
|
150
|
+
created: "2025-01-01T00:00:00Z",
|
|
151
|
+
updated: "2025-01-02T00:00:00Z",
|
|
152
|
+
description: "Test app",
|
|
153
|
+
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
154
|
+
dateFormat: "MM-DD-YYYY",
|
|
155
|
+
hasEveryoneOnTheInternet: false,
|
|
156
|
+
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
157
|
+
securityProperties: {
|
|
158
|
+
allowClone: false,
|
|
159
|
+
allowExport: true,
|
|
160
|
+
enableAppTokens: true,
|
|
161
|
+
hideFromPublic: false,
|
|
162
|
+
mustBeRealmApproved: false,
|
|
163
|
+
useIPFilter: false,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
mockFetch.mockResolvedValue({
|
|
168
|
+
ok: true,
|
|
169
|
+
status: 200,
|
|
170
|
+
json: () => Promise.resolve(mockResponse),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
await Promise.all([
|
|
175
|
+
client.getApp({ appId }),
|
|
176
|
+
client.getApp({ appId }),
|
|
177
|
+
client.getApp({ appId }),
|
|
178
|
+
client.getApp({ appId }),
|
|
179
|
+
client.getApp({ appId }),
|
|
180
|
+
client.getApp({ appId }), // 6th call triggers throttle
|
|
181
|
+
]);
|
|
182
|
+
const endTime = Date.now();
|
|
183
|
+
const durationMs = endTime - startTime;
|
|
184
|
+
|
|
185
|
+
expect(mockFetch).toHaveBeenCalledTimes(6);
|
|
186
|
+
expect(durationMs).toBeGreaterThanOrEqual(200);
|
|
187
|
+
expect(durationMs).toBeLessThan(1000);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// tests/vitest/unit/tables/updateRelationship.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
createClient,
|
|
6
|
+
mockFetch,
|
|
7
|
+
QB_REALM,
|
|
8
|
+
QB_USER_TOKEN,
|
|
9
|
+
QB_TABLE_ID_1,
|
|
10
|
+
} from "@tests/setup.ts";
|
|
11
|
+
import {
|
|
12
|
+
UpdateRelationshipRequest,
|
|
13
|
+
UpdateRelationship200Response,
|
|
14
|
+
} from "@/generated/models";
|
|
15
|
+
|
|
16
|
+
describe("QuickbaseClient Unit - updateRelationship", () => {
|
|
17
|
+
let client: ReturnType<typeof createClient>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockFetch.mockClear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("initializes without errors and has updateRelationship method", () => {
|
|
24
|
+
client = createClient(mockFetch, { debug: true });
|
|
25
|
+
expect(client).toBeDefined();
|
|
26
|
+
expect(typeof client.updateRelationship).toBe("function");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("updates relationship successfully with user token", async () => {
|
|
30
|
+
client = createClient(mockFetch, { debug: true });
|
|
31
|
+
|
|
32
|
+
const tableId = QB_TABLE_ID_1;
|
|
33
|
+
const relationshipId = 6;
|
|
34
|
+
|
|
35
|
+
const request: UpdateRelationshipRequest = {
|
|
36
|
+
lookupFieldIds: [4, 5, 6],
|
|
37
|
+
summaryFields: [
|
|
38
|
+
{
|
|
39
|
+
summaryFid: 3,
|
|
40
|
+
label: "my summary field",
|
|
41
|
+
accumulationType: "COUNT",
|
|
42
|
+
where: "{'3'.EX.'1'}",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mockResponse: UpdateRelationship200Response = {
|
|
48
|
+
id: relationshipId,
|
|
49
|
+
parentTableId: "bck7gp3q2",
|
|
50
|
+
childTableId: tableId,
|
|
51
|
+
foreignKeyField: {
|
|
52
|
+
id: relationshipId,
|
|
53
|
+
label: "Related record",
|
|
54
|
+
type: "numeric",
|
|
55
|
+
},
|
|
56
|
+
isCrossApp: false,
|
|
57
|
+
lookupFields: [
|
|
58
|
+
{ id: 4, label: "Field 4", type: "text" },
|
|
59
|
+
{ id: 5, label: "Field 5", type: "numeric" },
|
|
60
|
+
{ id: 6, label: "Field 6", type: "date" },
|
|
61
|
+
],
|
|
62
|
+
summaryFields: [{ id: 8, label: "my summary field", type: "numeric" }],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
mockFetch.mockResolvedValueOnce({
|
|
66
|
+
ok: true,
|
|
67
|
+
status: 200,
|
|
68
|
+
json: () => Promise.resolve(mockResponse),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await client.updateRelationship({
|
|
72
|
+
tableId,
|
|
73
|
+
relationshipId,
|
|
74
|
+
body: request,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(result).toEqual(mockResponse);
|
|
78
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
79
|
+
`https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: expect.objectContaining({
|
|
83
|
+
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
84
|
+
Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
}),
|
|
87
|
+
body: JSON.stringify(request),
|
|
88
|
+
credentials: "omit",
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("updates relationship successfully with temp token", async () => {
|
|
94
|
+
client = createClient(mockFetch, { useTempTokens: true, debug: true });
|
|
95
|
+
|
|
96
|
+
const tableId = QB_TABLE_ID_1;
|
|
97
|
+
const relationshipId = 6;
|
|
98
|
+
|
|
99
|
+
const request: UpdateRelationshipRequest = {
|
|
100
|
+
lookupFieldIds: [7],
|
|
101
|
+
summaryFields: [
|
|
102
|
+
{
|
|
103
|
+
summaryFid: 0,
|
|
104
|
+
label: "Count of Records",
|
|
105
|
+
accumulationType: "COUNT",
|
|
106
|
+
where: "{'3'.GT.0}",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const mockResponse: UpdateRelationship200Response = {
|
|
112
|
+
id: relationshipId,
|
|
113
|
+
parentTableId: "bck7gp3q2",
|
|
114
|
+
childTableId: tableId,
|
|
115
|
+
foreignKeyField: {
|
|
116
|
+
id: relationshipId,
|
|
117
|
+
label: "Related record",
|
|
118
|
+
type: "numeric",
|
|
119
|
+
},
|
|
120
|
+
isCrossApp: false,
|
|
121
|
+
lookupFields: [{ id: 7, label: "record - text field", type: "text" }],
|
|
122
|
+
summaryFields: [{ id: 9, label: "Count of Records", type: "numeric" }],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
mockFetch
|
|
126
|
+
.mockResolvedValueOnce({
|
|
127
|
+
ok: true,
|
|
128
|
+
status: 200,
|
|
129
|
+
json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
|
|
130
|
+
})
|
|
131
|
+
.mockResolvedValueOnce({
|
|
132
|
+
ok: true,
|
|
133
|
+
status: 200,
|
|
134
|
+
json: () => Promise.resolve(mockResponse),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = await client.updateRelationship({
|
|
138
|
+
tableId,
|
|
139
|
+
relationshipId,
|
|
140
|
+
body: request,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result).toEqual(mockResponse);
|
|
144
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
145
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
146
|
+
1,
|
|
147
|
+
`https://api.quickbase.com/v1/auth/temporary/${tableId}`,
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
method: "GET",
|
|
150
|
+
headers: expect.objectContaining({
|
|
151
|
+
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
152
|
+
"Content-Type": "application/json",
|
|
153
|
+
}),
|
|
154
|
+
credentials: "include",
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
158
|
+
2,
|
|
159
|
+
`https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: expect.objectContaining({
|
|
163
|
+
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
164
|
+
Authorization: "QB-TEMP-TOKEN temp_token",
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
}),
|
|
167
|
+
body: JSON.stringify(request),
|
|
168
|
+
credentials: "omit",
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("retries successfully after 401 with temp token", async () => {
|
|
174
|
+
client = createClient(mockFetch, { useTempTokens: true, debug: true });
|
|
175
|
+
|
|
176
|
+
const tableId = QB_TABLE_ID_1;
|
|
177
|
+
const relationshipId = 6;
|
|
178
|
+
|
|
179
|
+
const request: UpdateRelationshipRequest = {
|
|
180
|
+
lookupFieldIds: [4],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const mockResponse: UpdateRelationship200Response = {
|
|
184
|
+
id: relationshipId,
|
|
185
|
+
parentTableId: "bck7gp3q2",
|
|
186
|
+
childTableId: tableId,
|
|
187
|
+
foreignKeyField: {
|
|
188
|
+
id: relationshipId,
|
|
189
|
+
label: "Related record",
|
|
190
|
+
type: "numeric",
|
|
191
|
+
},
|
|
192
|
+
isCrossApp: false,
|
|
193
|
+
lookupFields: [{ id: 4, label: "Field 4", type: "text" }],
|
|
194
|
+
summaryFields: [],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
mockFetch
|
|
198
|
+
.mockResolvedValueOnce({
|
|
199
|
+
ok: true,
|
|
200
|
+
status: 200,
|
|
201
|
+
json: () =>
|
|
202
|
+
Promise.resolve({ temporaryAuthorization: "initial_token" }),
|
|
203
|
+
})
|
|
204
|
+
.mockResolvedValueOnce({
|
|
205
|
+
ok: false,
|
|
206
|
+
status: 401,
|
|
207
|
+
json: () => Promise.resolve({ message: "Unauthorized" }),
|
|
208
|
+
})
|
|
209
|
+
.mockResolvedValueOnce({
|
|
210
|
+
ok: true,
|
|
211
|
+
status: 200,
|
|
212
|
+
json: () => Promise.resolve({ temporaryAuthorization: "new_token" }),
|
|
213
|
+
})
|
|
214
|
+
.mockResolvedValueOnce({
|
|
215
|
+
ok: true,
|
|
216
|
+
status: 200,
|
|
217
|
+
json: () => Promise.resolve(mockResponse),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
221
|
+
const result = await client.updateRelationship({
|
|
222
|
+
tableId,
|
|
223
|
+
relationshipId,
|
|
224
|
+
body: request,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result).toEqual(mockResponse);
|
|
228
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
229
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
230
|
+
1,
|
|
231
|
+
`https://api.quickbase.com/v1/auth/temporary/${tableId}`,
|
|
232
|
+
expect.any(Object)
|
|
233
|
+
);
|
|
234
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
235
|
+
2,
|
|
236
|
+
`https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
|
|
237
|
+
expect.objectContaining({
|
|
238
|
+
headers: expect.objectContaining({
|
|
239
|
+
Authorization: "QB-TEMP-TOKEN initial_token",
|
|
240
|
+
}),
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
244
|
+
3,
|
|
245
|
+
`https://api.quickbase.com/v1/auth/temporary/${tableId}`,
|
|
246
|
+
expect.any(Object)
|
|
247
|
+
);
|
|
248
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
249
|
+
4,
|
|
250
|
+
`https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
|
|
251
|
+
expect.objectContaining({
|
|
252
|
+
headers: expect.objectContaining({
|
|
253
|
+
Authorization: "QB-TEMP-TOKEN new_token",
|
|
254
|
+
}),
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
258
|
+
"Authorization error for updateRelationship (temp token), refreshing token:",
|
|
259
|
+
expect.any(String)
|
|
260
|
+
);
|
|
261
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
262
|
+
"Retrying updateRelationship with temp token"
|
|
263
|
+
);
|
|
264
|
+
consoleSpy.mockRestore();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("handles 400 error for invalid request", async () => {
|
|
268
|
+
client = createClient(mockFetch, { debug: true });
|
|
269
|
+
|
|
270
|
+
const tableId = QB_TABLE_ID_1;
|
|
271
|
+
const relationshipId = 6;
|
|
272
|
+
|
|
273
|
+
const request: UpdateRelationshipRequest = {
|
|
274
|
+
lookupFieldIds: [-1],
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
mockFetch.mockResolvedValueOnce({
|
|
278
|
+
ok: false,
|
|
279
|
+
status: 400,
|
|
280
|
+
json: () => Promise.resolve({ message: "Invalid field ID" }),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await expect(
|
|
284
|
+
client.updateRelationship({
|
|
285
|
+
tableId,
|
|
286
|
+
relationshipId,
|
|
287
|
+
body: request,
|
|
288
|
+
})
|
|
289
|
+
).rejects.toThrow("API Error: Invalid field ID (Status: 400)");
|
|
290
|
+
|
|
291
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
292
|
+
`https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
|
|
293
|
+
expect.objectContaining({
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: expect.objectContaining({
|
|
296
|
+
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
297
|
+
Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
}),
|
|
300
|
+
body: JSON.stringify(request),
|
|
301
|
+
credentials: "omit",
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createClient, mockFetch, QB_APP_ID, QB_REALM } from "@tests/setup.ts";
|
|
3
|
+
import { UpdateAppRequest, UpdateApp200Response } from "@/generated/models";
|
|
4
|
+
|
|
5
|
+
describe("QuickbaseClient Unit - Token Lifespan", () => {
|
|
6
|
+
let client: ReturnType<typeof createClient>;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFetch.mockClear();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("reuses token within lifespan and fetches new token after expiration", async () => {
|
|
13
|
+
// Set a short lifespan of 500ms for testing
|
|
14
|
+
client = createClient(mockFetch, {
|
|
15
|
+
realm: QB_REALM,
|
|
16
|
+
useTempTokens: true,
|
|
17
|
+
tokenLifespan: 500, // 0.5 seconds
|
|
18
|
+
debug: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const request: UpdateAppRequest = {
|
|
22
|
+
name: "Test App",
|
|
23
|
+
};
|
|
24
|
+
const mockResponse: UpdateApp200Response = {
|
|
25
|
+
id: QB_APP_ID,
|
|
26
|
+
name: "Test App",
|
|
27
|
+
description: "",
|
|
28
|
+
created: new Date("2020-01-01T00:00:00Z"),
|
|
29
|
+
updated: new Date("2020-01-01T00:00:00Z"),
|
|
30
|
+
dateFormat: "MM-DD-YYYY",
|
|
31
|
+
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
32
|
+
hasEveryoneOnTheInternet: false,
|
|
33
|
+
ancestorId: null,
|
|
34
|
+
variables: [],
|
|
35
|
+
securityProperties: {},
|
|
36
|
+
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
37
|
+
dataClassification: "None",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// First call: Fetch a new token
|
|
41
|
+
mockFetch
|
|
42
|
+
.mockResolvedValueOnce({
|
|
43
|
+
ok: true,
|
|
44
|
+
status: 200,
|
|
45
|
+
json: () => Promise.resolve({ temporaryAuthorization: "token1" }),
|
|
46
|
+
})
|
|
47
|
+
.mockResolvedValueOnce({
|
|
48
|
+
ok: true,
|
|
49
|
+
status: 200,
|
|
50
|
+
json: () => Promise.resolve(mockResponse),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await client.updateApp({ appId: QB_APP_ID, body: request });
|
|
54
|
+
expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + API call
|
|
55
|
+
|
|
56
|
+
// Second call within lifespan: Should reuse token1
|
|
57
|
+
mockFetch.mockResolvedValueOnce({
|
|
58
|
+
ok: true,
|
|
59
|
+
status: 200,
|
|
60
|
+
json: () => Promise.resolve(mockResponse),
|
|
61
|
+
});
|
|
62
|
+
await client.updateApp({ appId: QB_APP_ID, body: request });
|
|
63
|
+
expect(mockFetch).toHaveBeenCalledTimes(3); // Only API call, no new token fetch
|
|
64
|
+
|
|
65
|
+
// Wait for token to expire (500ms + buffer)
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
67
|
+
|
|
68
|
+
// Third call after expiration: Should fetch a new token
|
|
69
|
+
mockFetch
|
|
70
|
+
.mockResolvedValueOnce({
|
|
71
|
+
ok: true,
|
|
72
|
+
status: 200,
|
|
73
|
+
json: () => Promise.resolve({ temporaryAuthorization: "token2" }),
|
|
74
|
+
})
|
|
75
|
+
.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
status: 200,
|
|
78
|
+
json: () => Promise.resolve(mockResponse),
|
|
79
|
+
});
|
|
80
|
+
await client.updateApp({ appId: QB_APP_ID, body: request });
|
|
81
|
+
expect(mockFetch).toHaveBeenCalledTimes(5); // New token fetch + API call
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses default lifespan when not specified", async () => {
|
|
85
|
+
// No tokenLifespan provided, should use default (4:50 = 290,000ms)
|
|
86
|
+
client = createClient(mockFetch, {
|
|
87
|
+
realm: QB_REALM,
|
|
88
|
+
useTempTokens: true,
|
|
89
|
+
debug: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const request: UpdateAppRequest = {
|
|
93
|
+
name: "Default Lifespan Test",
|
|
94
|
+
};
|
|
95
|
+
const mockResponse: UpdateApp200Response = {
|
|
96
|
+
id: QB_APP_ID,
|
|
97
|
+
name: "Default Lifespan Test",
|
|
98
|
+
description: "",
|
|
99
|
+
created: new Date("2020-01-01T00:00:00Z"),
|
|
100
|
+
updated: new Date("2020-01-01T00:00:00Z"),
|
|
101
|
+
dateFormat: "MM-DD-YYYY",
|
|
102
|
+
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
103
|
+
hasEveryoneOnTheInternet: false,
|
|
104
|
+
ancestorId: null,
|
|
105
|
+
variables: [],
|
|
106
|
+
securityProperties: {},
|
|
107
|
+
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
108
|
+
dataClassification: "None",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// First call: Fetch a new token
|
|
112
|
+
mockFetch
|
|
113
|
+
.mockResolvedValueOnce({
|
|
114
|
+
ok: true,
|
|
115
|
+
status: 200,
|
|
116
|
+
json: () =>
|
|
117
|
+
Promise.resolve({ temporaryAuthorization: "default_token" }),
|
|
118
|
+
})
|
|
119
|
+
.mockResolvedValueOnce({
|
|
120
|
+
ok: true,
|
|
121
|
+
status: 200,
|
|
122
|
+
json: () => Promise.resolve(mockResponse),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await client.updateApp({ appId: QB_APP_ID, body: request });
|
|
126
|
+
expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + API call
|
|
127
|
+
|
|
128
|
+
// Second call within default lifespan (e.g., after 1 second, well below 4:50)
|
|
129
|
+
mockFetch.mockResolvedValueOnce({
|
|
130
|
+
ok: true,
|
|
131
|
+
status: 200,
|
|
132
|
+
json: () => Promise.resolve(mockResponse),
|
|
133
|
+
});
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
|
135
|
+
await client.updateApp({ appId: QB_APP_ID, body: request });
|
|
136
|
+
expect(mockFetch).toHaveBeenCalledTimes(3); // No new token fetch, still within 4:50
|
|
137
|
+
});
|
|
138
|
+
});
|
package/tools/log-gen.ts
CHANGED
|
@@ -18,20 +18,20 @@ const excludeDirs: string[] = [
|
|
|
18
18
|
// prettier-ignore
|
|
19
19
|
const includeFolders: string[] = [
|
|
20
20
|
// "open-api",
|
|
21
|
-
// "open-api/definitions",
|
|
22
|
-
// "open-api/paths",
|
|
23
21
|
];
|
|
24
22
|
|
|
25
23
|
// prettier-ignore
|
|
26
24
|
const includeRecursiveFolders: string[] = [
|
|
27
|
-
"open-api"
|
|
25
|
+
// "open-api"
|
|
28
26
|
// "tests",
|
|
29
27
|
// "tests/vitest/unit",
|
|
30
|
-
|
|
28
|
+
"tests/vitest/qb"
|
|
31
29
|
];
|
|
32
30
|
|
|
33
31
|
// prettier-ignore
|
|
34
32
|
const includeFiles: string[] = [
|
|
33
|
+
// "tests/setup.ts",
|
|
34
|
+
|
|
35
35
|
// "package.json",
|
|
36
36
|
// "tsconfig.json",
|
|
37
37
|
// "tsconfig.build.json",
|
|
@@ -43,7 +43,11 @@ const includeFiles: string[] = [
|
|
|
43
43
|
// "src/tokenCache.ts",
|
|
44
44
|
// "src/generated/runtime.ts",
|
|
45
45
|
// "src/quickbaseClient.ts",
|
|
46
|
+
// "src/invokeMethod.ts",
|
|
46
47
|
// "src/generated-unified/QuickbaseClient.ts",
|
|
48
|
+
|
|
49
|
+
// "src/RateLimitError.ts",
|
|
50
|
+
// "src/TokenBucket.ts"
|
|
47
51
|
];
|
|
48
52
|
|
|
49
53
|
const projectGoals: string[] = [
|