quickbase-js 1.1.0-beta.1 → 1.1.0-beta.3

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.
@@ -1,6 +1,10 @@
1
1
  // tests/vitest/qb/records/upsert.test.ts
2
2
  import { describe, test, expect, beforeEach, afterEach } from "vitest";
3
3
  import { quickbase } from "@/quickbaseClient.ts";
4
+ import {
5
+ Upsert200Response,
6
+ Upsert207Response,
7
+ } from "/home/drew/Projects/quickbase-js/src/generated/models";
4
8
 
5
9
  const TABLE_ID = "buwai2zws";
6
10
  const MARKER_FIELD_ID = 6; // Text field for marking test records
@@ -39,7 +43,7 @@ describe("QuickbaseClient - Upsert Integration", () => {
39
43
  test(
40
44
  "creates a new record successfully",
41
45
  async () => {
42
- const response = await qb.upsert({
46
+ const response: Upsert200Response | Upsert207Response = await qb.upsert({
43
47
  body: {
44
48
  to: TABLE_ID,
45
49
  data: [{ [MARKER_FIELD_ID]: { value: "test-marker-upsert" } }],
@@ -47,12 +51,38 @@ describe("QuickbaseClient - Upsert Integration", () => {
47
51
  },
48
52
  });
49
53
 
54
+ // Check metadata with null safety
55
+ expect(
56
+ response.metadata,
57
+ "Expected metadata to be defined"
58
+ ).toBeDefined();
59
+ if (!response.metadata) {
60
+ throw new Error("Expected metadata to be present in upsert response");
61
+ }
62
+
63
+ expect(
64
+ response.metadata.createdRecordIds,
65
+ "Expected createdRecordIds to be defined"
66
+ ).toBeDefined();
67
+ if (!response.metadata.createdRecordIds) {
68
+ throw new Error(
69
+ "Expected createdRecordIds to be present in upsert response"
70
+ );
71
+ }
50
72
  expect(response.metadata.createdRecordIds.length).toBe(1);
73
+
74
+ // Check data with null safety
75
+ expect(response.data, "Expected data to be defined").toBeDefined();
76
+ if (!response.data || response.data.length === 0) {
77
+ throw new Error("Expected at least one record in data");
78
+ }
51
79
  expect(response.data).toHaveLength(1);
52
- expect(response.data[0][MARKER_FIELD_ID].value).toBe(
80
+ expect(response.data[0][MARKER_FIELD_ID]?.value).toBe(
53
81
  "test-marker-upsert"
54
82
  );
55
- expect(response.data[0][3].value).toBeDefined(); // Record ID assigned by QuickBase
83
+ expect(response.data[0][3]?.value).toBeDefined(); // Record ID assigned by QuickBase
84
+
85
+ // Check totalNumberOfRecordsProcessed
56
86
  expect(response.metadata.totalNumberOfRecordsProcessed).toBe(1);
57
87
  },
58
88
  { timeout: 10000 }
@@ -0,0 +1,80 @@
1
+ // tests/vitest/qb/tables/getRelationships.test.ts
2
+ import { describe, test, expect } from "vitest";
3
+ import { quickbase } from "/home/drew/Projects/quickbase-js/src/quickbaseClient.ts";
4
+ import { QB_TABLE_ID_1 } from "/home/drew/Projects/quickbase-js/tests/setup.ts";
5
+ import { GetRelationships200Response } from "/home/drew/Projects/quickbase-js/src/generated/models";
6
+
7
+ describe("QuickbaseClient Integration - getRelationships", () => {
8
+ const qb = quickbase({
9
+ realm: process.env.QB_REALM || "builderprogram-dbradford6815",
10
+ userToken: process.env.QB_USER_TOKEN || "",
11
+ debug: true,
12
+ });
13
+
14
+ test("fetches real relationships from QuickBase", async () => {
15
+ // Ensure environment variables are set
16
+ if (!process.env.QB_REALM)
17
+ throw new Error("QB_REALM is not defined in .env");
18
+ if (!process.env.QB_USER_TOKEN)
19
+ throw new Error("QB_USER_TOKEN is not defined in .env");
20
+
21
+ console.log("Config used:", {
22
+ realm: process.env.QB_REALM,
23
+ userToken: process.env.QB_USER_TOKEN,
24
+ tableId: QB_TABLE_ID_1,
25
+ });
26
+
27
+ const response: GetRelationships200Response = await qb.getRelationships({
28
+ tableId: QB_TABLE_ID_1, // Query relationships for QB_TABLE_ID_1 (e.g., "buwai2zws")
29
+ skip: 0,
30
+ });
31
+
32
+ console.log("Real API response:", JSON.stringify(response, null, 2));
33
+
34
+ // Basic structure validation
35
+ expect(response).toBeDefined();
36
+ expect(response.metadata).toBeDefined();
37
+ expect(response.metadata).toHaveProperty(
38
+ "numRelationships",
39
+ expect.any(Number)
40
+ );
41
+ expect(response.metadata).toHaveProperty("skip", 0);
42
+ expect(response.metadata).toHaveProperty(
43
+ "totalRelationships",
44
+ expect.any(Number)
45
+ );
46
+ expect(response.relationships).toBeDefined();
47
+ expect(Array.isArray(response.relationships)).toBe(true);
48
+
49
+ // If relationships exist, validate the first one
50
+ if (response.relationships.length > 0) {
51
+ const relationship = response.relationships[0];
52
+ expect(relationship).toHaveProperty("id", expect.any(Number));
53
+ expect(relationship).toHaveProperty("parentTableId", expect.any(String));
54
+ expect(relationship).toHaveProperty("childTableId", QB_TABLE_ID_1);
55
+ expect(relationship).toHaveProperty("foreignKeyField");
56
+ expect(relationship.foreignKeyField).toHaveProperty(
57
+ "id",
58
+ expect.any(Number)
59
+ );
60
+ expect(relationship.foreignKeyField).toHaveProperty(
61
+ "label",
62
+ expect.any(String)
63
+ );
64
+ expect(relationship.foreignKeyField).toHaveProperty(
65
+ "type",
66
+ expect.any(String)
67
+ );
68
+ expect(relationship).toHaveProperty("isCrossApp", expect.any(Boolean));
69
+ expect(relationship).toHaveProperty("lookupFields", expect.any(Array));
70
+
71
+ // summaryFields is optional; only validate if present
72
+ if (
73
+ "summaryFields" in relationship &&
74
+ relationship.summaryFields !== undefined
75
+ ) {
76
+ expect(Array.isArray(relationship.summaryFields)).toBe(true);
77
+ }
78
+ }
79
+ }, 10000); // Timeout as second argument
80
+ });
@@ -0,0 +1,282 @@
1
+ // tests/vitest/unit/apps/copyApp.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 { CopyAppRequest, CopyApp200Response } from "@/generated/models";
11
+
12
+ describe("QuickbaseClient Unit - copyApp", () => {
13
+ let client: ReturnType<typeof createClient>;
14
+
15
+ beforeEach(() => {
16
+ mockFetch.mockClear();
17
+ });
18
+
19
+ it("initializes without errors", () => {
20
+ client = createClient(mockFetch, { debug: true });
21
+ expect(client).toBeDefined();
22
+ });
23
+
24
+ it("has copyApp method", () => {
25
+ client = createClient(mockFetch, { debug: true });
26
+ expect(typeof client.copyApp).toBe("function");
27
+ });
28
+
29
+ it("calls copyApp successfully with user token", async () => {
30
+ client = createClient(mockFetch, { debug: true });
31
+
32
+ const request: CopyAppRequest = {
33
+ name: "Copied App",
34
+ description: "A copy of the original app",
35
+ properties: {
36
+ keepData: false,
37
+ excludeFiles: true,
38
+ usersAndRoles: false,
39
+ assignUserToken: true,
40
+ },
41
+ };
42
+
43
+ const mockResponse: CopyApp200Response = {
44
+ id: "bpqe82s1",
45
+ name: "Copied App",
46
+ description: "A copy of the original app",
47
+ created: new Date("2025-03-11T10:00:00Z"), // Changed to Date object
48
+ updated: new Date("2025-03-11T10:00:00Z"), // Changed to Date object
49
+ dateFormat: "MM-DD-YYYY",
50
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
51
+ hasEveryoneOnTheInternet: false,
52
+ ancestorId: QB_APP_ID,
53
+ dataClassification: "None",
54
+ variables: [],
55
+ };
56
+
57
+ mockFetch.mockResolvedValueOnce({
58
+ ok: true,
59
+ status: 200,
60
+ json: () =>
61
+ Promise.resolve({
62
+ ...mockResponse,
63
+ created: "2025-03-11T10:00:00Z", // API returns string
64
+ updated: "2025-03-11T10:00:00Z", // API returns string
65
+ }),
66
+ });
67
+
68
+ const result = await client.copyApp({ appId: QB_APP_ID, body: request });
69
+
70
+ expect(result).toEqual(mockResponse);
71
+ expect(mockFetch).toHaveBeenCalledWith(
72
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}/copy`,
73
+ expect.objectContaining({
74
+ method: "POST",
75
+ headers: expect.objectContaining({
76
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
77
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
78
+ "Content-Type": "application/json",
79
+ }),
80
+ body: JSON.stringify(request),
81
+ })
82
+ );
83
+ });
84
+
85
+ it("calls copyApp successfully with temp token", async () => {
86
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
87
+
88
+ const request: CopyAppRequest = {
89
+ name: "Copied App with Temp Token",
90
+ description: "A copy using temp token",
91
+ properties: {
92
+ keepData: true,
93
+ excludeFiles: false,
94
+ usersAndRoles: true,
95
+ assignUserToken: false,
96
+ },
97
+ };
98
+
99
+ const mockResponse: CopyApp200Response = {
100
+ id: "bpqe82s2",
101
+ name: "Copied App with Temp Token",
102
+ description: "A copy using temp token",
103
+ created: new Date("2025-03-11T11:00:00Z"), // Changed to Date object
104
+ updated: new Date("2025-03-11T11:00:00Z"), // Changed to Date object
105
+ dateFormat: "MM-DD-YYYY",
106
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
107
+ hasEveryoneOnTheInternet: true,
108
+ ancestorId: QB_APP_ID,
109
+ dataClassification: "None",
110
+ variables: [],
111
+ };
112
+
113
+ mockFetch
114
+ .mockResolvedValueOnce({
115
+ ok: true,
116
+ status: 200,
117
+ json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
118
+ })
119
+ .mockResolvedValueOnce({
120
+ ok: true,
121
+ status: 200,
122
+ json: () =>
123
+ Promise.resolve({
124
+ ...mockResponse,
125
+ created: "2025-03-11T11:00:00Z", // API returns string
126
+ updated: "2025-03-11T11:00:00Z", // API returns string
127
+ }),
128
+ });
129
+
130
+ const result = await client.copyApp({ appId: QB_APP_ID, body: request });
131
+
132
+ expect(result).toEqual(mockResponse);
133
+ expect(mockFetch).toHaveBeenCalledTimes(2);
134
+ expect(mockFetch).toHaveBeenNthCalledWith(
135
+ 1,
136
+ `https://api.quickbase.com/v1/auth/temporary/${QB_APP_ID}`,
137
+ expect.objectContaining({
138
+ method: "GET",
139
+ headers: expect.objectContaining({
140
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
141
+ "Content-Type": "application/json",
142
+ }),
143
+ credentials: "include",
144
+ })
145
+ );
146
+ expect(mockFetch).toHaveBeenNthCalledWith(
147
+ 2,
148
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}/copy`,
149
+ expect.objectContaining({
150
+ method: "POST",
151
+ headers: expect.objectContaining({
152
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
153
+ Authorization: "QB-TEMP-TOKEN temp_token",
154
+ "Content-Type": "application/json",
155
+ }),
156
+ body: JSON.stringify(request),
157
+ })
158
+ );
159
+ });
160
+
161
+ it("retries successfully after 401 with temp token", async () => {
162
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
163
+
164
+ const request: CopyAppRequest = {
165
+ name: "Retry Copied App",
166
+ description: "Retry after 401",
167
+ properties: {
168
+ keepData: false,
169
+ excludeFiles: true,
170
+ usersAndRoles: false,
171
+ assignUserToken: true,
172
+ },
173
+ };
174
+
175
+ const mockResponse: CopyApp200Response = {
176
+ id: "bpqe82s3",
177
+ name: "Retry Copied App",
178
+ description: "Retry after 401",
179
+ created: new Date("2025-03-11T12:00:00Z"), // Changed to Date object
180
+ updated: new Date("2025-03-11T12:00:00Z"), // Changed to Date object
181
+ dateFormat: "MM-DD-YYYY",
182
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
183
+ hasEveryoneOnTheInternet: false,
184
+ ancestorId: QB_APP_ID,
185
+ dataClassification: "None",
186
+ variables: [],
187
+ };
188
+
189
+ mockFetch
190
+ .mockResolvedValueOnce({
191
+ ok: true,
192
+ status: 200,
193
+ json: () =>
194
+ Promise.resolve({ temporaryAuthorization: "initial_token" }),
195
+ })
196
+ .mockResolvedValueOnce({
197
+ ok: false,
198
+ status: 401,
199
+ json: () => Promise.resolve({ message: "Unauthorized" }),
200
+ })
201
+ .mockResolvedValueOnce({
202
+ ok: true,
203
+ status: 200,
204
+ json: () => Promise.resolve({ temporaryAuthorization: "new_token" }),
205
+ })
206
+ .mockResolvedValueOnce({
207
+ ok: true,
208
+ status: 200,
209
+ json: () =>
210
+ Promise.resolve({
211
+ ...mockResponse,
212
+ created: "2025-03-11T12:00:00Z", // API returns string
213
+ updated: "2025-03-11T12:00:00Z", // API returns string
214
+ }),
215
+ });
216
+
217
+ const result = await client.copyApp({ appId: QB_APP_ID, body: request });
218
+
219
+ expect(result).toEqual(mockResponse);
220
+ expect(mockFetch).toHaveBeenCalledTimes(4);
221
+ expect(mockFetch).toHaveBeenNthCalledWith(
222
+ 1,
223
+ `https://api.quickbase.com/v1/auth/temporary/${QB_APP_ID}`,
224
+ expect.any(Object)
225
+ );
226
+ expect(mockFetch).toHaveBeenNthCalledWith(
227
+ 2,
228
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}/copy`,
229
+ expect.objectContaining({
230
+ headers: expect.objectContaining({
231
+ Authorization: "QB-TEMP-TOKEN initial_token",
232
+ }),
233
+ })
234
+ );
235
+ expect(mockFetch).toHaveBeenNthCalledWith(
236
+ 3,
237
+ `https://api.quickbase.com/v1/auth/temporary/${QB_APP_ID}`,
238
+ expect.any(Object)
239
+ );
240
+ expect(mockFetch).toHaveBeenNthCalledWith(
241
+ 4,
242
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}/copy`,
243
+ expect.objectContaining({
244
+ headers: expect.objectContaining({
245
+ Authorization: "QB-TEMP-TOKEN new_token",
246
+ }),
247
+ })
248
+ );
249
+ });
250
+
251
+ it("handles 400 error for invalid request", async () => {
252
+ client = createClient(mockFetch, { debug: true });
253
+
254
+ const request: CopyAppRequest = {
255
+ name: "", // Invalid: name is required
256
+ description: "Invalid request",
257
+ };
258
+
259
+ mockFetch.mockResolvedValueOnce({
260
+ ok: false,
261
+ status: 400,
262
+ json: () => Promise.resolve({ message: "Name is required" }),
263
+ });
264
+
265
+ await expect(
266
+ client.copyApp({ appId: QB_APP_ID, body: request })
267
+ ).rejects.toThrow("API Error: Name is required (Status: 400)");
268
+
269
+ expect(mockFetch).toHaveBeenCalledWith(
270
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}/copy`,
271
+ expect.objectContaining({
272
+ method: "POST",
273
+ headers: expect.objectContaining({
274
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
275
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
276
+ "Content-Type": "application/json",
277
+ }),
278
+ body: JSON.stringify(request),
279
+ })
280
+ );
281
+ });
282
+ });
@@ -0,0 +1,125 @@
1
+ // tests/vitest/unit/apps/createApp.test.ts
2
+ import { describe, it, expect, beforeEach } from "vitest";
3
+ import {
4
+ createClient,
5
+ mockFetch,
6
+ QB_REALM,
7
+ QB_USER_TOKEN,
8
+ } from "@tests/setup.ts";
9
+ import { CreateAppRequest, CreateApp200Response } from "@/generated/models";
10
+
11
+ describe("QuickbaseClient Unit - createApp", () => {
12
+ let client: ReturnType<typeof createClient>;
13
+
14
+ beforeEach(() => {
15
+ mockFetch.mockClear();
16
+ });
17
+
18
+ it("initializes without errors", () => {
19
+ client = createClient(mockFetch, { debug: true });
20
+ expect(client).toBeDefined();
21
+ });
22
+
23
+ it("has createApp method", () => {
24
+ client = createClient(mockFetch, { debug: true });
25
+ expect(typeof client.createApp).toBe("function");
26
+ });
27
+
28
+ it("calls createApp successfully with user token", async () => {
29
+ client = createClient(mockFetch, { debug: true });
30
+
31
+ const request: CreateAppRequest = {
32
+ name: "New App",
33
+ description: "A new app for testing",
34
+ assignToken: true,
35
+ };
36
+
37
+ const mockResponse: CreateApp200Response = {
38
+ id: "new-app-id",
39
+ name: "New App",
40
+ description: "A new app for testing",
41
+ created: new Date("2025-03-11T10:00:00Z"),
42
+ updated: new Date("2025-03-11T10:00:00Z"),
43
+ dateFormat: "MM-DD-YYYY",
44
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
45
+ hasEveryoneOnTheInternet: false,
46
+ variables: [],
47
+ dataClassification: "None",
48
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
49
+ securityProperties: {
50
+ allowClone: false,
51
+ allowExport: true,
52
+ enableAppTokens: true,
53
+ hideFromPublic: false,
54
+ mustBeRealmApproved: false,
55
+ useIPFilter: false,
56
+ },
57
+ };
58
+
59
+ mockFetch.mockResolvedValueOnce({
60
+ ok: true,
61
+ status: 200,
62
+ json: () =>
63
+ Promise.resolve({
64
+ ...mockResponse,
65
+ created: "2025-03-11T10:00:00Z", // API returns string
66
+ updated: "2025-03-11T10:00:00Z", // API returns string
67
+ }),
68
+ });
69
+
70
+ const result = await client.createApp({ body: request });
71
+
72
+ expect(result).toEqual(mockResponse);
73
+ expect(mockFetch).toHaveBeenCalledWith(
74
+ `https://api.quickbase.com/v1/apps`,
75
+ expect.objectContaining({
76
+ method: "POST",
77
+ headers: expect.objectContaining({
78
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
79
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
80
+ "Content-Type": "application/json",
81
+ }),
82
+ body: expect.stringContaining('"name":"New App"'),
83
+ credentials: "omit", // Added to match actual behavior
84
+ })
85
+ );
86
+ // Additional check to ensure body contains all properties regardless of order
87
+ const callArgs = mockFetch.mock.calls[0];
88
+ const body = JSON.parse(callArgs[1].body as string);
89
+ expect(body).toEqual(request);
90
+ });
91
+
92
+ it("handles 400 error for invalid request", async () => {
93
+ client = createClient(mockFetch, { debug: true });
94
+
95
+ const request: CreateAppRequest = {
96
+ name: "", // Invalid: name is required
97
+ };
98
+
99
+ mockFetch.mockImplementationOnce(() =>
100
+ Promise.resolve({
101
+ ok: false,
102
+ status: 400,
103
+ json: () => Promise.resolve({ message: "Name is required" }),
104
+ })
105
+ );
106
+
107
+ await expect(client.createApp({ body: request })).rejects.toThrow(
108
+ "API Error: Name is required (Status: 400)"
109
+ );
110
+
111
+ expect(mockFetch).toHaveBeenCalledWith(
112
+ `https://api.quickbase.com/v1/apps`,
113
+ expect.objectContaining({
114
+ method: "POST",
115
+ headers: expect.objectContaining({
116
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
117
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
118
+ "Content-Type": "application/json",
119
+ }),
120
+ body: JSON.stringify(request),
121
+ credentials: "omit", // Added to match actual behavior
122
+ })
123
+ );
124
+ });
125
+ });