quickbase-js 0.1.0-beta.13 → 0.1.0-beta.14
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/README.md +1 -1
- package/dist/esm/quickbase.d.ts +2 -1
- package/package.json +9 -4
- package/CHANGELOG.md +0 -239
- package/build-common.js +0 -131
- package/build-esm.js +0 -71
- package/build-umd.js +0 -69
- package/docs/.vitepress/cache/deps/@theme_index.js +0 -275
- package/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
- package/docs/.vitepress/cache/deps/_metadata.json +0 -40
- package/docs/.vitepress/cache/deps/chunk-5A67QEFC.js +0 -12542
- package/docs/.vitepress/cache/deps/chunk-5A67QEFC.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-7D4P4MMP.js +0 -9719
- package/docs/.vitepress/cache/deps/chunk-7D4P4MMP.js.map +0 -7
- package/docs/.vitepress/cache/deps/package.json +0 -3
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4504
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/docs/.vitepress/cache/deps/vue.js +0 -343
- package/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/docs/.vitepress/config.mts +0 -26
- package/docs/api-examples.md +0 -49
- package/docs/index.md +0 -59
- package/docs/markdown-examples.md +0 -85
- package/docs-data/api-docs.json +0 -4143
- package/examples/quicbase-jsCDN.html +0 -36
- package/examples/react.js +0 -48
- package/examples/vanillajs.html +0 -47
- package/index.md +0 -25
- package/open-api/fix-spec-main.ts +0 -133
- package/open-api/generate-unified-interface.ts +0 -104
- package/open-api/generateDocsJson.ts +0 -24
- package/open-api/output/quickbase-fixed.json +0 -11191
- package/open-api/post-process-generated.ts +0 -226
- package/open-api/regenerate-client.ts +0 -117
- package/open-api/schema/enhance-general.ts +0 -111
- package/open-api/schema/fix-arrays.ts +0 -68
- package/open-api/schema/tags/apps.ts +0 -90
- package/open-api/schema/tags/fields.ts +0 -471
- package/open-api/schema/tags/groups.ts +0 -51
- package/open-api/schema/tags/index.ts +0 -66
- package/open-api/schema/tags/records.ts +0 -115
- package/open-api/schema/tags/tables.ts +0 -211
- package/open-api/schema/tags/users.ts +0 -47
- package/open-api/types/spec.ts +0 -42
- package/open-api/utils/common.ts +0 -29
- package/open-api/utils/generateJsDoc.ts +0 -74
- package/open-api/utils/infer-schema.ts +0 -40
- package/open-api/utils/naming.ts +0 -10
- package/open-api/utils/sharedUtils.ts +0 -352
- package/playwright.config.ts +0 -37
- package/rollup.config.js +0 -114
- package/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +0 -46878
- package/specs/split/apps.json +0 -1315
- package/specs/split/auth.json +0 -191
- package/specs/split/fields.json +0 -3869
- package/specs/split/records.json +0 -754
- package/specs/split/reports.json +0 -619
- package/specs/split/tables.json +0 -1605
- package/src/BurstAwareThrottleBucket.ts +0 -57
- package/src/FlowThrottleBucket.ts +0 -47
- package/src/RateLimitError.ts +0 -12
- package/src/RateThrottleBucket.ts +0 -5
- package/src/Semaphore.ts +0 -33
- package/src/authorizationStrategy.ts +0 -321
- package/src/generated-unified/QuickbaseClient.ts +0 -1135
- package/src/generated-unified/missing-types-report.json +0 -3
- package/src/index.ts +0 -1
- package/src/invokeMethod.ts +0 -290
- package/src/pagination.ts +0 -429
- package/src/quickbaseClient.ts +0 -227
- package/src/rateLimiter.ts +0 -30
- package/src/tokenCache.ts +0 -39
- package/src/utils.ts +0 -50
- package/tests/playwright/qb/auth/cacheSwitching.test.ts +0 -159
- package/tests/playwright/qb/auth/fetchTempToken401.test.ts +0 -219
- package/tests/playwright/qb/auth/getAppWithTempToken.test.ts +0 -162
- package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +0 -208
- package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +0 -153
- package/tests/playwright/qb/auth/getTempToken.test.ts +0 -152
- package/tests/playwright/qb/auth/tempTokenPrefetch.test.ts +0 -179
- package/tests/playwright/qb/auth/tempTokenRenewal.test.ts +0 -133
- package/tests/setup.ts +0 -53
- package/tests/vitest/qb/apps/copyApp.test.ts +0 -76
- package/tests/vitest/qb/apps/createApp.test.ts +0 -64
- package/tests/vitest/qb/apps/deleteApp.test.ts +0 -76
- package/tests/vitest/qb/apps/getApp.test.ts +0 -44
- package/tests/vitest/qb/apps/getAppEvents.test.ts +0 -67
- package/tests/vitest/qb/apps/updateApp.test.ts +0 -58
- package/tests/vitest/qb/auth/mockQuickbaseServer.ts +0 -60
- package/tests/vitest/qb/auth/retryUserToken401.test.ts +0 -73
- package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +0 -68
- package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +0 -65
- package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +0 -63
- package/tests/vitest/qb/fields/createField.test.ts +0 -77
- package/tests/vitest/qb/fields/deleteFields.test.ts +0 -83
- package/tests/vitest/qb/fields/getField.test.ts +0 -85
- package/tests/vitest/qb/fields/getFieldUsage.test.ts +0 -82
- package/tests/vitest/qb/fields/getFields.test.ts +0 -232
- package/tests/vitest/qb/fields/getFieldsUsage.test.ts +0 -77
- package/tests/vitest/qb/fields/updateField.test.ts +0 -93
- package/tests/vitest/qb/formulas/runFormula.test.ts +0 -96
- package/tests/vitest/qb/index.ts +0 -29
- package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +0 -125
- package/tests/vitest/qb/quickbase-js/FlowBucket.test.ts +0 -77
- package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +0 -111
- package/tests/vitest/qb/records/deleteRecords.test.ts +0 -110
- package/tests/vitest/qb/records/runQuery.test.ts +0 -145
- package/tests/vitest/qb/records/upsert.test.ts +0 -102
- package/tests/vitest/qb/tables/createRelationship.test.ts +0 -102
- package/tests/vitest/qb/tables/createTable.test.ts +0 -55
- package/tests/vitest/qb/tables/deleteRelationship.test.ts +0 -143
- package/tests/vitest/qb/tables/deleteTable.test.ts +0 -50
- package/tests/vitest/qb/tables/getAppTables.test.ts +0 -46
- package/tests/vitest/qb/tables/getRelationships.test.ts +0 -80
- package/tests/vitest/qb/tables/getTable.test.ts +0 -26
- package/tests/vitest/qb/tables/updateRelationship.test.ts +0 -250
- package/tests/vitest/qb/tables/updateTable.test.ts +0 -48
- package/tests/vitest/unit/apps/copyApp.test.ts +0 -581
- package/tests/vitest/unit/apps/createApp.test.ts +0 -186
- package/tests/vitest/unit/apps/deleteApp.test.ts +0 -216
- package/tests/vitest/unit/apps/getApp.test.ts +0 -128
- package/tests/vitest/unit/apps/getAppConcurrency.test.ts +0 -151
- package/tests/vitest/unit/apps/getAppEvents.test.ts +0 -193
- package/tests/vitest/unit/apps/updateApp.test.ts +0 -412
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +0 -81
- package/tests/vitest/unit/auth/getTempToken.test.ts +0 -119
- package/tests/vitest/unit/auth/retryTempToken401.test.ts +0 -95
- package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +0 -206
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +0 -66
- package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +0 -325
- package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +0 -62
- package/tests/vitest/unit/fields/createField.test.ts +0 -451
- package/tests/vitest/unit/fields/deleteFields.test.ts +0 -141
- package/tests/vitest/unit/fields/getField.test.ts +0 -189
- package/tests/vitest/unit/fields/getFieldUsage.test.ts +0 -180
- package/tests/vitest/unit/fields/getFields.test.ts +0 -122
- package/tests/vitest/unit/fields/getFieldsUsage.test.ts +0 -160
- package/tests/vitest/unit/fields/updateField.test.ts +0 -285
- package/tests/vitest/unit/formulas/runFormula.test.ts +0 -205
- package/tests/vitest/unit/index.ts +0 -34
- package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +0 -108
- package/tests/vitest/unit/quickbase-js/FlowBucket.test.ts +0 -101
- package/tests/vitest/unit/quickbase-js/pagination.test.ts +0 -251
- package/tests/vitest/unit/quickbase-js/quickbaseClientHttpMethods.test.ts +0 -141
- package/tests/vitest/unit/quickbase-js/rateLimit.test.ts +0 -191
- package/tests/vitest/unit/quickbase-js/tokenLifespan.test.ts +0 -204
- package/tests/vitest/unit/records/deleteRecords.test.ts +0 -112
- package/tests/vitest/unit/records/runQuery.test.ts +0 -101
- package/tests/vitest/unit/records/upsert.test.ts +0 -234
- package/tests/vitest/unit/tables/createRelationship.test.ts +0 -212
- package/tests/vitest/unit/tables/createTable.test.ts +0 -70
- package/tests/vitest/unit/tables/deleteRelationship.test.ts +0 -97
- package/tests/vitest/unit/tables/deleteTable.test.ts +0 -77
- package/tests/vitest/unit/tables/getAppTables.test.ts +0 -85
- package/tests/vitest/unit/tables/getRelationships.test.ts +0 -259
- package/tests/vitest/unit/tables/getTable.test.ts +0 -233
- package/tests/vitest/unit/tables/updateRelationship.test.ts +0 -307
- package/tests/vitest/unit/tables/updateTable.test.ts +0 -80
- package/tools/log-gen.ts +0 -251
- package/tools/log-gen.yaml +0 -4829
- package/tools/openapi-jars/openapi-generator-cli-7.12.0.jar +0 -0
- package/tsconfig.build.json +0 -17
- package/tsconfig.json +0 -32
- package/versionrc.json +0 -9
- package/vitest.config.ts +0 -27
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// test/vitest/unit/ThrottleBucket.test.ts
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
import { FlowThrottleBucket } from "@/FlowThrottleBucket"; // Should work with your alias
|
|
4
|
-
|
|
5
|
-
describe("ConcurrentThrottleBucket Unit Tests", () => {
|
|
6
|
-
it("allows burst capacity immediately", async () => {
|
|
7
|
-
const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
|
|
8
|
-
const startTime = Date.now();
|
|
9
|
-
|
|
10
|
-
await Promise.all([
|
|
11
|
-
bucket.acquire().then(() => bucket.release()),
|
|
12
|
-
bucket.acquire().then(() => bucket.release()),
|
|
13
|
-
bucket.acquire().then(() => bucket.release()),
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
const duration = Date.now() - startTime;
|
|
17
|
-
expect(duration).toBeLessThan(50); // All 3 should be instant (within 50ms)
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("delays calls beyond burst at correct rate", async () => {
|
|
21
|
-
const bucket = new FlowThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
|
|
22
|
-
const timings: number[] = [];
|
|
23
|
-
const recordTime = async () => {
|
|
24
|
-
await bucket.acquire();
|
|
25
|
-
timings.push(Date.now());
|
|
26
|
-
bucket.release(); // Release slot
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const startTime = Date.now();
|
|
30
|
-
await Promise.all([
|
|
31
|
-
recordTime(),
|
|
32
|
-
recordTime(),
|
|
33
|
-
recordTime(),
|
|
34
|
-
recordTime(), // 4th call should delay
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const durations = timings.map((t) => t - startTime);
|
|
38
|
-
expect(durations.length).toBe(4);
|
|
39
|
-
expect(durations[0]).toBeLessThan(50); // 1st call instant
|
|
40
|
-
expect(durations[1]).toBeLessThan(50); // 2nd call instant
|
|
41
|
-
expect(durations[2]).toBeLessThan(50); // 3rd call instant
|
|
42
|
-
expect(durations[3]).toBeGreaterThanOrEqual(180); // 4th call delayed ~200ms
|
|
43
|
-
expect(durations[3]).toBeLessThan(250); // Shouldn’t exceed 250ms
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("maintains rate over sustained calls", async () => {
|
|
47
|
-
const bucket = new FlowThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
|
|
48
|
-
const timings: number[] = [];
|
|
49
|
-
const recordTime = async () => {
|
|
50
|
-
await bucket.acquire();
|
|
51
|
-
timings.push(Date.now());
|
|
52
|
-
bucket.release(); // Release slot
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const startTime = Date.now();
|
|
56
|
-
await Promise.all([
|
|
57
|
-
recordTime(),
|
|
58
|
-
recordTime(),
|
|
59
|
-
recordTime(),
|
|
60
|
-
recordTime(),
|
|
61
|
-
recordTime(), // 5 calls total
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
const durations = timings.map((t) => t - startTime);
|
|
65
|
-
expect(durations.length).toBe(5);
|
|
66
|
-
expect(durations[0]).toBeLessThan(50); // 1st instant
|
|
67
|
-
expect(durations[1]).toBeLessThan(50); // 2nd instant
|
|
68
|
-
expect(durations[2]).toBeGreaterThanOrEqual(450); // 3rd delayed ~500ms (1/2 sec)
|
|
69
|
-
expect(durations[2]).toBeLessThan(550);
|
|
70
|
-
expect(durations[3]).toBeGreaterThanOrEqual(950); // 4th delayed ~1000ms
|
|
71
|
-
expect(durations[3]).toBeLessThan(1050);
|
|
72
|
-
expect(durations[4]).toBeGreaterThanOrEqual(1450); // 5th delayed ~1500ms
|
|
73
|
-
expect(durations[4]).toBeLessThan(1550);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("handles high concurrency without exceeding burst", async () => {
|
|
77
|
-
const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
|
|
78
|
-
const timings: number[] = [];
|
|
79
|
-
const recordTime = async () => {
|
|
80
|
-
await bucket.acquire();
|
|
81
|
-
timings.push(Date.now());
|
|
82
|
-
bucket.release(); // Release slot
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const startTime = Date.now();
|
|
86
|
-
await Promise.all(
|
|
87
|
-
Array(10)
|
|
88
|
-
.fill(null)
|
|
89
|
-
.map(() => recordTime())
|
|
90
|
-
); // 10 concurrent calls
|
|
91
|
-
|
|
92
|
-
const durations = timings.map((t) => t - startTime);
|
|
93
|
-
expect(durations.length).toBe(10);
|
|
94
|
-
expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true); // First 3 instant
|
|
95
|
-
for (let i = 3; i < 10; i++) {
|
|
96
|
-
const expectedDelay = (i - 2) * 200; // After burst, 200ms per token
|
|
97
|
-
expect(durations[i]).toBeGreaterThanOrEqual(expectedDelay - 50);
|
|
98
|
-
expect(durations[i]).toBeLessThan(expectedDelay + 50);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
});
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { paginateRecords, isPaginatable } from "@/pagination";
|
|
3
|
-
import { QuickbaseClient } from "@/quickbaseClient";
|
|
4
|
-
import * as invokeMethodModule from "@/invokeMethod";
|
|
5
|
-
import { GetUsers200Response, GetUsersRequest } from "@/generated/models";
|
|
6
|
-
|
|
7
|
-
// Extend GetUsersRequest to include nextPageToken
|
|
8
|
-
interface TestGetUsersRequest extends GetUsersRequest {
|
|
9
|
-
nextPageToken?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe("Pagination Unit Tests", () => {
|
|
13
|
-
const methodMap = {
|
|
14
|
-
runQuery: {
|
|
15
|
-
api: {},
|
|
16
|
-
method: vi.fn(),
|
|
17
|
-
paramMap: ["body"],
|
|
18
|
-
httpMethod: "POST",
|
|
19
|
-
},
|
|
20
|
-
getUsers: {
|
|
21
|
-
api: {},
|
|
22
|
-
method: vi.fn(),
|
|
23
|
-
paramMap: ["body"],
|
|
24
|
-
httpMethod: "POST",
|
|
25
|
-
},
|
|
26
|
-
} as any;
|
|
27
|
-
const baseHeaders = {};
|
|
28
|
-
const authStrategy = {
|
|
29
|
-
getToken: vi.fn(),
|
|
30
|
-
applyHeaders: vi.fn(),
|
|
31
|
-
handleError: vi.fn(),
|
|
32
|
-
} as any;
|
|
33
|
-
const rateLimiter = {
|
|
34
|
-
throttle: vi.fn(),
|
|
35
|
-
release: vi.fn(),
|
|
36
|
-
maxRetries: 3,
|
|
37
|
-
} as any;
|
|
38
|
-
const transformDates = (obj: any) => obj;
|
|
39
|
-
|
|
40
|
-
it("fetches all pages for runQuery with skip-based pagination", async () => {
|
|
41
|
-
const mockInvoke = vi
|
|
42
|
-
.spyOn(invokeMethodModule, "invokeMethod")
|
|
43
|
-
.mockResolvedValueOnce({
|
|
44
|
-
data: Array(2429).fill({ "3": { value: "record" } }),
|
|
45
|
-
metadata: {
|
|
46
|
-
totalRecords: 5000,
|
|
47
|
-
numRecords: 2429,
|
|
48
|
-
numFields: 6,
|
|
49
|
-
skip: 0,
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
.mockResolvedValueOnce({
|
|
53
|
-
data: Array(2429).fill({ "3": { value: "record" } }),
|
|
54
|
-
metadata: {
|
|
55
|
-
totalRecords: 5000,
|
|
56
|
-
numRecords: 2429,
|
|
57
|
-
numFields: 6,
|
|
58
|
-
skip: 2429,
|
|
59
|
-
},
|
|
60
|
-
})
|
|
61
|
-
.mockResolvedValueOnce({
|
|
62
|
-
data: Array(142).fill({ "3": { value: "record" } }),
|
|
63
|
-
metadata: {
|
|
64
|
-
totalRecords: 5000,
|
|
65
|
-
numRecords: 142,
|
|
66
|
-
numFields: 6,
|
|
67
|
-
skip: 4858,
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const result = await paginateRecords(
|
|
72
|
-
"runQuery",
|
|
73
|
-
{ body: { from: "mock-table", select: [3, 6] } },
|
|
74
|
-
methodMap,
|
|
75
|
-
baseHeaders,
|
|
76
|
-
authStrategy,
|
|
77
|
-
rateLimiter,
|
|
78
|
-
transformDates,
|
|
79
|
-
false,
|
|
80
|
-
true
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
expect(mockInvoke).toHaveBeenCalledTimes(3);
|
|
84
|
-
expect(result.data.length).toBe(5000);
|
|
85
|
-
expect(result.metadata).toEqual({
|
|
86
|
-
totalRecords: 5000,
|
|
87
|
-
numRecords: 5000,
|
|
88
|
-
numFields: 6,
|
|
89
|
-
skip: 0,
|
|
90
|
-
top: undefined,
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("fetches all pages for getUsers with token-based pagination", async () => {
|
|
95
|
-
const mockInvoke = vi
|
|
96
|
-
.spyOn(invokeMethodModule, "invokeMethod")
|
|
97
|
-
.mockImplementation(
|
|
98
|
-
async <K extends keyof QuickbaseClient>(
|
|
99
|
-
methodName: K,
|
|
100
|
-
params: Parameters<QuickbaseClient[K]>[0] & {
|
|
101
|
-
skip?: number;
|
|
102
|
-
top?: number;
|
|
103
|
-
},
|
|
104
|
-
methodMap: any,
|
|
105
|
-
baseHeaders: any,
|
|
106
|
-
authStrategy: any,
|
|
107
|
-
rateLimiter: any,
|
|
108
|
-
transformDates: any,
|
|
109
|
-
debug: any,
|
|
110
|
-
convertDates: any,
|
|
111
|
-
autoPaginate: any,
|
|
112
|
-
attempt: any,
|
|
113
|
-
maxAttempts: any,
|
|
114
|
-
isPaginating: any
|
|
115
|
-
): Promise<ReturnType<QuickbaseClient[K]>> => {
|
|
116
|
-
if (methodName !== "getUsers")
|
|
117
|
-
throw new Error("Mock only supports 'getUsers'");
|
|
118
|
-
const body = (params as { body?: TestGetUsersRequest }).body;
|
|
119
|
-
const token = body?.nextPageToken;
|
|
120
|
-
if (!token) {
|
|
121
|
-
return {
|
|
122
|
-
users: Array(10).fill({ id: "user1" }),
|
|
123
|
-
metadata: { nextPageToken: "token1" },
|
|
124
|
-
} as GetUsers200Response as ReturnType<QuickbaseClient[K]>;
|
|
125
|
-
} else if (token === "token1") {
|
|
126
|
-
return {
|
|
127
|
-
users: Array(5).fill({ id: "user2" }),
|
|
128
|
-
metadata: { nextPageToken: "token2" },
|
|
129
|
-
} as GetUsers200Response as ReturnType<QuickbaseClient[K]>;
|
|
130
|
-
} else if (token === "token2") {
|
|
131
|
-
return {
|
|
132
|
-
users: Array(3).fill({ id: "user3" }),
|
|
133
|
-
metadata: { nextPageToken: "" },
|
|
134
|
-
} as GetUsers200Response as ReturnType<QuickbaseClient[K]>;
|
|
135
|
-
}
|
|
136
|
-
throw new Error("Unexpected token");
|
|
137
|
-
}
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const result = await paginateRecords(
|
|
141
|
-
"getUsers",
|
|
142
|
-
{ body: { userIds: ["id1", "id2"] } },
|
|
143
|
-
methodMap,
|
|
144
|
-
baseHeaders,
|
|
145
|
-
authStrategy,
|
|
146
|
-
rateLimiter,
|
|
147
|
-
transformDates,
|
|
148
|
-
false,
|
|
149
|
-
true
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
expect(mockInvoke).toHaveBeenCalledTimes(3);
|
|
153
|
-
expect(mockInvoke).toHaveBeenNthCalledWith(
|
|
154
|
-
1,
|
|
155
|
-
"getUsers",
|
|
156
|
-
expect.objectContaining({ body: { userIds: ["id1", "id2"] } }),
|
|
157
|
-
methodMap,
|
|
158
|
-
baseHeaders,
|
|
159
|
-
authStrategy,
|
|
160
|
-
rateLimiter,
|
|
161
|
-
transformDates,
|
|
162
|
-
false,
|
|
163
|
-
true,
|
|
164
|
-
false,
|
|
165
|
-
0,
|
|
166
|
-
4,
|
|
167
|
-
true
|
|
168
|
-
);
|
|
169
|
-
expect(mockInvoke).toHaveBeenNthCalledWith(
|
|
170
|
-
2,
|
|
171
|
-
"getUsers",
|
|
172
|
-
expect.objectContaining({
|
|
173
|
-
body: { userIds: ["id1", "id2"], nextPageToken: "token1" },
|
|
174
|
-
}),
|
|
175
|
-
methodMap,
|
|
176
|
-
baseHeaders,
|
|
177
|
-
authStrategy,
|
|
178
|
-
rateLimiter,
|
|
179
|
-
transformDates,
|
|
180
|
-
false,
|
|
181
|
-
true,
|
|
182
|
-
false,
|
|
183
|
-
0,
|
|
184
|
-
4,
|
|
185
|
-
true
|
|
186
|
-
);
|
|
187
|
-
expect(mockInvoke).toHaveBeenNthCalledWith(
|
|
188
|
-
3,
|
|
189
|
-
"getUsers",
|
|
190
|
-
expect.objectContaining({
|
|
191
|
-
body: { userIds: ["id1", "id2"], nextPageToken: "token2" },
|
|
192
|
-
}),
|
|
193
|
-
methodMap,
|
|
194
|
-
baseHeaders,
|
|
195
|
-
authStrategy,
|
|
196
|
-
rateLimiter,
|
|
197
|
-
transformDates,
|
|
198
|
-
false,
|
|
199
|
-
true,
|
|
200
|
-
false,
|
|
201
|
-
0,
|
|
202
|
-
4,
|
|
203
|
-
true
|
|
204
|
-
);
|
|
205
|
-
expect(result.users.length).toBe(18);
|
|
206
|
-
expect(result.metadata).toEqual({
|
|
207
|
-
numRecords: 18,
|
|
208
|
-
numFields: undefined,
|
|
209
|
-
nextPageToken: "",
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("stops when all skip-based records are fetched in one page", async () => {
|
|
214
|
-
const mockInvoke = vi
|
|
215
|
-
.spyOn(invokeMethodModule, "invokeMethod")
|
|
216
|
-
.mockResolvedValueOnce({
|
|
217
|
-
data: Array(10).fill({ id: "record" }),
|
|
218
|
-
metadata: { totalRecords: 10, numRecords: 10, numFields: 3, skip: 0 },
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const result = await paginateRecords(
|
|
222
|
-
"runQuery",
|
|
223
|
-
{ body: { from: "mock-table", select: [3] } },
|
|
224
|
-
methodMap,
|
|
225
|
-
baseHeaders,
|
|
226
|
-
authStrategy,
|
|
227
|
-
rateLimiter,
|
|
228
|
-
transformDates,
|
|
229
|
-
false,
|
|
230
|
-
true
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
|
234
|
-
expect(result.data.length).toBe(10);
|
|
235
|
-
expect(result.metadata.totalRecords).toBe(10);
|
|
236
|
-
expect(result.metadata.numRecords).toBe(10);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("isPaginatable detects metadata correctly", () => {
|
|
240
|
-
const skipResponse = {
|
|
241
|
-
data: [],
|
|
242
|
-
metadata: { totalRecords: 100, numRecords: 25, numFields: 3, skip: 0 },
|
|
243
|
-
};
|
|
244
|
-
const tokenResponse = { users: [], metadata: { nextPageToken: "abc" } };
|
|
245
|
-
const invalidResponse = { data: [], other: "stuff" };
|
|
246
|
-
|
|
247
|
-
expect(isPaginatable(skipResponse)).toBe(true);
|
|
248
|
-
expect(isPaginatable(tokenResponse)).toBe(true);
|
|
249
|
-
expect(isPaginatable(invalidResponse)).toBe(false);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { quickbase } from "@/quickbaseClient";
|
|
3
|
-
import { mockFetch, QB_APP_ID, QB_REALM } from "../../../setup.ts";
|
|
4
|
-
import {
|
|
5
|
-
UpdateAppRequest,
|
|
6
|
-
UpdateApp200Response,
|
|
7
|
-
} from "../../../../src/generated/models/index.ts";
|
|
8
|
-
import { ChangesetSolutionFromRecord200Response } from "../../../../src/generated/models/index.ts";
|
|
9
|
-
|
|
10
|
-
describe("QuickbaseClient Unit - HTTP Method Extraction", () => {
|
|
11
|
-
let clientTemp: ReturnType<typeof quickbase>;
|
|
12
|
-
let clientUser: ReturnType<typeof quickbase>;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
mockFetch.mockClear();
|
|
16
|
-
vi.useFakeTimers();
|
|
17
|
-
|
|
18
|
-
clientTemp = quickbase({
|
|
19
|
-
realm: QB_REALM,
|
|
20
|
-
userToken: "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
|
|
21
|
-
debug: false,
|
|
22
|
-
fetchApi: mockFetch,
|
|
23
|
-
throttle: { rate: 10, burst: 10 },
|
|
24
|
-
useTempTokens: true,
|
|
25
|
-
tempTokenLifespan: 500,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
clientUser = quickbase({
|
|
29
|
-
realm: QB_REALM,
|
|
30
|
-
userToken: "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
|
|
31
|
-
debug: false,
|
|
32
|
-
fetchApi: mockFetch,
|
|
33
|
-
throttle: { rate: 10, burst: 10 },
|
|
34
|
-
useTempTokens: false,
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterEach(() => {
|
|
39
|
-
vi.useRealTimers();
|
|
40
|
-
mockFetch.mockReset();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const mockUpdateAppResponse: UpdateApp200Response = {
|
|
44
|
-
id: QB_APP_ID,
|
|
45
|
-
name: "Test App",
|
|
46
|
-
description: "",
|
|
47
|
-
created: new Date("2020-01-01T00:00:00Z"),
|
|
48
|
-
updated: new Date("2020-01-01T00:00:00Z"),
|
|
49
|
-
dateFormat: "MM-DD-YYYY",
|
|
50
|
-
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
51
|
-
hasEveryoneOnTheInternet: false,
|
|
52
|
-
ancestorId: undefined,
|
|
53
|
-
variables: [],
|
|
54
|
-
securityProperties: {},
|
|
55
|
-
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
56
|
-
dataClassification: "None",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const mockChangesetResponse: ChangesetSolutionFromRecord200Response = [
|
|
60
|
-
{ changes: [] },
|
|
61
|
-
];
|
|
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("uses POST for updateApp", async () => {
|
|
88
|
-
setupMockFetch();
|
|
89
|
-
await clientTemp.updateApp({
|
|
90
|
-
appId: QB_APP_ID,
|
|
91
|
-
body: { name: "Test App" },
|
|
92
|
-
});
|
|
93
|
-
const apiCall = mockFetch.mock.calls.find((call) =>
|
|
94
|
-
call[0].includes("/apps/")
|
|
95
|
-
)![1];
|
|
96
|
-
expect(apiCall.method).toBe("POST");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("uses POST for runReport", async () => {
|
|
100
|
-
setupMockFetch();
|
|
101
|
-
await clientTemp.runReport({
|
|
102
|
-
tableId: QB_APP_ID,
|
|
103
|
-
reportId: "report1",
|
|
104
|
-
body: { skip: 0, top: 10 },
|
|
105
|
-
});
|
|
106
|
-
const apiCall = mockFetch.mock.calls.find((call) =>
|
|
107
|
-
call[0].includes("/reports/")
|
|
108
|
-
)![1];
|
|
109
|
-
expect(apiCall.method).toBe("POST");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("uses GET for changesetSolutionFromRecord", async () => {
|
|
113
|
-
setupMockFetchForChangeset();
|
|
114
|
-
await clientUser.changesetSolutionFromRecord({
|
|
115
|
-
solutionId: "solution1",
|
|
116
|
-
tableId: "table1",
|
|
117
|
-
fieldId: 6,
|
|
118
|
-
recordId: "123",
|
|
119
|
-
});
|
|
120
|
-
const apiCall = mockFetch.mock.calls[0][1];
|
|
121
|
-
expect(apiCall.method).toBe("GET");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("uses GET for getTable", async () => {
|
|
125
|
-
setupMockFetch();
|
|
126
|
-
await clientTemp.getTable({ appId: QB_APP_ID, tableId: "table1" });
|
|
127
|
-
const apiCall = mockFetch.mock.calls.find((call) =>
|
|
128
|
-
call[0].includes("/tables/")
|
|
129
|
-
)![1];
|
|
130
|
-
expect(apiCall.method).toBe("GET");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("uses DELETE for deleteTable", async () => {
|
|
134
|
-
setupMockFetch();
|
|
135
|
-
await clientTemp.deleteTable({ appId: QB_APP_ID, tableId: "table1" });
|
|
136
|
-
const apiCall = mockFetch.mock.calls.find((call) =>
|
|
137
|
-
call[0].includes("/tables/")
|
|
138
|
-
)![1];
|
|
139
|
-
expect(apiCall.method).toBe("DELETE");
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
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
|
-
maxRetries: 0, // Prevent retries for this test
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const appId = QB_APP_ID;
|
|
147
|
-
|
|
148
|
-
const mockResponse: GetApp200Response = {
|
|
149
|
-
id: appId,
|
|
150
|
-
name: "TestApp",
|
|
151
|
-
created: "2025-01-01T00:00:00Z",
|
|
152
|
-
updated: "2025-01-02T00:00:00Z",
|
|
153
|
-
description: "Test app",
|
|
154
|
-
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
155
|
-
dateFormat: "MM-DD-YYYY",
|
|
156
|
-
hasEveryoneOnTheInternet: false,
|
|
157
|
-
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
158
|
-
securityProperties: {
|
|
159
|
-
allowClone: false,
|
|
160
|
-
allowExport: true,
|
|
161
|
-
enableAppTokens: true,
|
|
162
|
-
hideFromPublic: false,
|
|
163
|
-
mustBeRealmApproved: false,
|
|
164
|
-
useIPFilter: false,
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
mockFetch.mockResolvedValue({
|
|
169
|
-
ok: true,
|
|
170
|
-
status: 200,
|
|
171
|
-
json: () => Promise.resolve(mockResponse),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const startTime = Date.now();
|
|
175
|
-
await Promise.all([
|
|
176
|
-
client.getApp({ appId }),
|
|
177
|
-
client.getApp({ appId }),
|
|
178
|
-
client.getApp({ appId }),
|
|
179
|
-
client.getApp({ appId }),
|
|
180
|
-
client.getApp({ appId }),
|
|
181
|
-
client.getApp({ appId }), // 6th call should be throttled
|
|
182
|
-
]);
|
|
183
|
-
const endTime = Date.now();
|
|
184
|
-
const durationMs = endTime - startTime;
|
|
185
|
-
|
|
186
|
-
console.log(`[Test Debug] mockFetch calls: ${mockFetch.mock.calls.length}`);
|
|
187
|
-
expect(mockFetch).toHaveBeenCalledTimes(6);
|
|
188
|
-
expect(durationMs).toBeGreaterThanOrEqual(200); // 1/5 sec for 6th call
|
|
189
|
-
expect(durationMs).toBeLessThan(1000);
|
|
190
|
-
});
|
|
191
|
-
});
|