quickbase-js 1.0.1-beta.0 → 1.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{quickbaseClient.d.ts → quickbase.d.ts} +344 -339
  3. package/dist/{quickbaseClient.js → quickbase.js} +350 -266
  4. package/dist/quickbase.js.map +1 -0
  5. package/log-gen.yaml +3565 -0
  6. package/package.json +11 -7
  7. package/rollup.config.js +2 -2
  8. package/src/code-generation/fix-spec-definitions.ts +417 -68
  9. package/src/code-generation/fix-spec-paths.ts +207 -2
  10. package/src/code-generation/output/quickbase-fixed.json +646 -1122
  11. package/src/generated-unified/QuickbaseClient.ts +9 -8
  12. package/src/log-gen.ts +82 -35
  13. package/src/quickbaseClient.ts +39 -42
  14. package/test-artifacts/test-results.json +173 -1145
  15. package/tests/playwright/qb/auth/cacheSwitching.test.ts +2 -2
  16. package/tests/playwright/qb/auth/fetchTempToken401.test.ts +2 -2
  17. package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +2 -2
  18. package/tests/playwright/qb/auth/getTempToken.test.ts +2 -2
  19. package/tests/playwright/qb/auth/tokenPrefetch.test.ts +2 -2
  20. package/tests/playwright/qb/auth/tokenRenewal.test.ts +2 -2
  21. package/tests/setup.ts +16 -8
  22. package/tests/vitest/qb/apps/getApp.test.ts +37 -43
  23. package/tests/vitest/qb/records/deleteRecords.test.ts +78 -0
  24. package/tests/vitest/qb/records/upsert.test.ts +72 -0
  25. package/tests/vitest/qb/tables/createTable.test.ts +55 -0
  26. package/tests/vitest/qb/tables/deleteTable.test.ts +50 -0
  27. package/tests/vitest/qb/tables/getAppTables.test.ts +43 -64
  28. package/tests/vitest/qb/tables/getTable.test.ts +26 -0
  29. package/tests/vitest/qb/tables/updateTable.test.ts +48 -0
  30. package/tests/vitest/unit/apps/getApp.test.ts +1 -1
  31. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +4 -3
  32. package/tests/vitest/unit/auth/getTempToken.test.ts +8 -9
  33. package/tests/vitest/unit/auth/retryOn401.test.ts +1 -1
  34. package/tests/vitest/unit/auth/two401s.test.ts +1 -1
  35. package/tests/vitest/unit/fields/getFields.test.ts +1 -1
  36. package/tests/vitest/unit/records/deleteRecords.test.ts +118 -0
  37. package/tests/vitest/unit/records/upsert.test.ts +228 -0
  38. package/tests/vitest/unit/tables/createTable.test.ts +62 -0
  39. package/tests/vitest/unit/tables/deleteTable.test.ts +68 -0
  40. package/tests/vitest/unit/tables/getAppTables.test.ts +1 -1
  41. package/tests/vitest/unit/tables/getTable.test.ts +212 -0
  42. package/tests/vitest/unit/tables/updateTable.test.ts +74 -0
  43. package/tsconfig.json +3 -2
  44. package/vitest.config.ts +3 -2
  45. package/dist/quickbaseClient.js.map +0 -1
  46. package/logGen.yaml +0 -1820
  47. package/src/utils.ts +0 -5
@@ -1,15 +1,16 @@
1
- // Generated on 2025-03-10T01:23:11.216Z
2
- import { App, Field, GetTempTokenDBID200Response, Record, ReportRunResponse, Table, Upsert200Response, Upsert207Response } from "../generated/models";
1
+ // Generated on 2025-03-11T02:50:18.896Z
2
+ import { App, CreateApp200Response, CreateAppRequest, DeleteRecords200Response, DeleteRecordsRequest, DeleteTableResponse, Field, GetTempTokenDBID200Response, Record, ReportRunResponse, Table, UpdateTableRequest, Upsert200Response, Upsert207Response, UpsertRequest } from "../generated/models";
3
3
 
4
4
  export interface QuickbaseClient {
5
- createApp: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
5
+ createApp: (params: { body: CreateAppRequest }) => Promise<CreateApp200Response>;
6
6
  getApp: (params: { appId: string }) => Promise<App>;
7
7
  getAppEvents: (params: { appId: string }) => Promise<any[]>;
8
8
  copyApp: (params: { appId: string; generated?: any }) => Promise<{ [key: string]: any }>;
9
9
  getAppTables: (params: { appId: string }) => Promise<Table[]>;
10
- getTable: (params: { appId: string; tableId: string }) => Promise<{ [key: string]: any }>;
11
- updateTable: (params: { appId: string; tableId: string; generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
12
- deleteTable: (params: { appId: string; tableId: string }) => Promise<{ [key: string]: any }>;
10
+ createTable: (params: { appId: string; body: any }) => Promise<Table>;
11
+ getTable: (params: { tableId: string; appId: string }) => Promise<Table>;
12
+ updateTable: (params: { tableId: string; appId: string; body: UpdateTableRequest }) => Promise<Table>;
13
+ deleteTable: (params: { tableId: string; appId: string }) => Promise<DeleteTableResponse>;
13
14
  getRelationships: (params: { skip?: number; tableId: string }) => Promise<{ [key: string]: any }>;
14
15
  createRelationship: (params: { tableId: string; generated?: any }) => Promise<{ [key: string]: any }>;
15
16
  updateRelationship: (params: { tableId: string; relationshipId: any; generated?: any }) => Promise<{ [key: string]: any }>;
@@ -23,8 +24,8 @@ export interface QuickbaseClient {
23
24
  getFieldsUsage: (params: { tableId: string; skip?: number; top?: number }) => Promise<any[]>;
24
25
  getFieldUsage: (params: { tableId: string; fieldId: number }) => Promise<any[]>;
25
26
  runFormula: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
26
- upsert: (params: { generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
27
- deleteRecords: (params: { generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
27
+ upsert: (params: { body: UpsertRequest }) => Promise<Upsert200Response | Upsert207Response>;
28
+ deleteRecords: (params: { body: DeleteRecordsRequest }) => Promise<DeleteRecords200Response>;
28
29
  upsertRecords: (params: { generated: { [key: string]: string }[] }) => Promise<Upsert200Response[] | Upsert207Response[]>;
29
30
  getTempTokenDBID: (params: { dbid: string }) => Promise<GetTempTokenDBID200Response>;
30
31
  exchangeSsoToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
package/src/log-gen.ts CHANGED
@@ -2,47 +2,79 @@
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
- import yaml from "js-yaml"; // Requires "js-yaml": "^4.1.0" in package.json
5
+ import yaml from "js-yaml";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
9
 
10
10
  const rootDir: string = "./";
11
- const excludeDirs: string[] = ["node_modules", ".git"];
12
-
13
- const importantFolders: string[] = [];
14
-
15
- const importantFiles: string[] = [
16
- "package.json",
17
- "tsconfig.json",
18
- "rollup.config.js",
19
- "vitest.config.ts",
20
- "generated-unified/QuickbaseClient.ts",
21
- "src/quickbaseClient.ts",
22
- "src/tokenCache.ts",
23
- "src/code-generation/fix-spec-main.ts",
24
- "src/code-generation/fix-spec-paths.ts",
25
- "src/code-generation/fix-spec-definitions.ts",
26
- "src/code-generation/regenerate-client.ts",
27
- "src/code-generation/generate-unified-interface.ts",
11
+ const excludeDirs: string[] = ["node_modules", ".git", "specs"];
12
+
13
+ // prettier-ignore
14
+ const includeFolders: string[] = [
15
+ // "src/code-generation",
16
+ ];
17
+
18
+ // prettier-ignore
19
+ const includeRecursiveFolders: string[] = [
20
+ "tests",
21
+ ];
22
+
23
+ // prettier-ignore
24
+ const includeFiles: string[] = [
25
+ // "package.json",
26
+ // "tsconfig.json",
27
+ // "rollup.config.js",
28
+ // "vitest.config.ts",
29
+ // "src/utils.ts",
30
+ // "src/tokenCache.ts",
31
+ // "src/generated-unified/QuickbaseClient.ts",
32
+ // "src/quickbaseClient.ts",
28
33
  ];
29
34
 
30
35
  const projectGoals: string[] = [
31
- "Implement a QuickBase API client in TypeScript.",
32
- "Generate client methods from the QuickBase RESTful API spec.",
33
- "Provide a simple and intuitive interface for interacting with QuickBase.",
34
- "Support both Node.js and browser environments.",
36
+ "A library for interacting with the QuickBase RESTful API.",
37
+ "Use OpenAPI to generate types and methods using a proxy. Validate with tests.",
38
+ "Provide robust and intuitive temporary token support for browser environments.",
39
+ "User Tokens support both Node.js and browser environments.",
35
40
  "Use case: JS frameworks like React, Vue, and Angular hosted in QuickBase code pages.",
36
- "Node.js is supported using User Tokens",
37
- "Use OpenAPI to generate the TypeScript types and methods.",
38
- "Use the latest ES6+ features and TypeScript features.",
41
+ "Enable a flexible or native fetch in the browser.",
42
+ "Enable a flexible fetch framework in Node.js.",
39
43
  "#",
40
44
  "No manual updating methods when the QuickBase API changes.",
41
- "The proxy magic dynamically maps generated methods.",
42
- "No manual endpoint definitions—endpoints come from the generated apis (e.g., AppsApi.getApp).",
45
+ "The proxy magic dynamically maps generated methods and types.",
43
46
  "#",
44
- "Per-Instance Store: tokenCache is defined per quickbaseClient call, so each instance has its own isolated TokenCache.",
45
- "No Singleton: No shared state—tokens are fetched and cached within the current instance.",
47
+ "TokenCache is defined per quickbaseClient call, so each instance has its own isolated TokenCache.",
48
+ "Each quickbase client instance has an isolated and individual token cache.",
49
+ ];
50
+
51
+ const pipelineOverview: string[] = [
52
+ "fix-spec-main.ts: Input: Reads the latest QuickBase_RESTful*.json file from the specs/ folder and applies fixes to the parameters and paths.",
53
+ "filters out the QB-Realm-Hostname, Authorization, and User-Agent parameters.",
54
+ "Converts parameter names to camelCase.",
55
+ "Fixes array schemas and applies custom paths.",
56
+ "Merges paths from fix-spec-paths.ts and definitions from fix-spec-definitions.ts.",
57
+ "Output: writes quickbase-fixed.json to src/code-generation/output/.",
58
+ "Key Behavior: The merge (spec.paths = { ...spec.paths, ...paths }) preserves all origional endpoints, only overriding those defined in fix-spec-paths.ts.",
59
+ "#",
60
+ "regenerate-client.ts: Uses quickbase-fixed.json to generate raw TypeScript-fetch files (src/generated/). including models and APIs.",
61
+ "#",
62
+ "generate-unified-interface.ts: Uses quickbase-fixed.json to generate a unified QuickbaseClient.ts interface in src/generated-unified/.",
63
+ "QuickbaseClient.ts includes all endpoints from src/code-generation/output/quickbase-fixed, and types from src/generated.",
64
+ "#",
65
+ "/specs/QuickBase_RESTful_API_*.json is over 46k lines of JSON, so it's not included in the snapshot, its too large to give to an AI.",
66
+ "which creates difficulties in understanding the structure of the API.",
67
+ "It makes it difficult to model the src/code-generation/fix-spec-*.ts files in the snapshot.",
68
+ "#",
69
+ "vitest unit and integration tests. Integration tests use the real QuickBase API with user token auth.",
70
+ "playwright is used to test the real QuickBase API in a browser enviornment with temporary token auth.",
71
+ "temp tokens can only be generated in a browser enviornment and can not be fetched with a user token.",
72
+ "#",
73
+ "getTempTokenDBID() method is wrapped to enhance temporary token generation reuse interacting with a token cache.",
74
+ "#",
75
+ "npm run fix-spec: Generates quickbase-fixed.json from the latest QuickBase_RESTful*.json file.",
76
+ "npm run regenerate: Generates raw TypeScript-fetch files in src/generated/.",
77
+ "npm run generate-unified: Generates a unified QuickbaseClient.ts interface in src/generated-unified/.",
46
78
  ];
47
79
 
48
80
  interface TreeNode {
@@ -77,11 +109,25 @@ function buildTree(dir: string): TreeNode {
77
109
  if (file.isDirectory()) {
78
110
  childNode.children = buildTree(filePath).children;
79
111
  } else {
80
- const isInImportantFolder = importantFolders.some(
112
+ const isInIncludeFolder = includeFolders.some((folder) => {
113
+ const folderPath = folder.endsWith("/") ? folder : folder + "/";
114
+ return (
115
+ relativePath.startsWith(folderPath) &&
116
+ relativePath.split("/").length === folderPath.split("/").length
117
+ );
118
+ });
119
+
120
+ const isInIncludeRecursiveFolder = includeRecursiveFolders.some(
81
121
  (folder) =>
82
122
  relativePath.startsWith(folder + "/") || relativePath === folder
83
123
  );
84
- if (importantFiles.includes(relativePath) || isInImportantFolder) {
124
+
125
+ const shouldInclude =
126
+ includeFiles.includes(relativePath) ||
127
+ isInIncludeFolder ||
128
+ isInIncludeRecursiveFolder;
129
+
130
+ if (shouldInclude) {
85
131
  try {
86
132
  childNode.contents = fs.readFileSync(filePath, "utf8");
87
133
  } catch (err: unknown) {
@@ -104,17 +150,18 @@ function generateTreeSnapshot(): string {
104
150
  date: new Date().toLocaleDateString(),
105
151
  root: path.resolve(rootDir),
106
152
  goals: projectGoals,
153
+ pipelineOverview: pipelineOverview,
107
154
  tree: buildTree(rootDir),
108
155
  };
109
156
 
110
- return yaml.dump(snapshot, { lineWidth: -1 }); // No line wrapping for readability
157
+ return yaml.dump(snapshot, { lineWidth: -1 });
111
158
  }
112
159
 
113
160
  try {
114
161
  const output: string = generateTreeSnapshot();
115
- fs.writeFileSync("logGen.yaml", output, "utf8");
116
- const lineCount: number = output.split("\n").length - 1; // Exclude trailing newline
117
- console.log("logGen.yaml");
162
+ fs.writeFileSync("log-gen.yaml", output, "utf8");
163
+ const lineCount: number = output.split("\n").length - 1;
164
+ console.log("log-gen.yaml");
118
165
  console.log(`Lines: ${lineCount}`);
119
166
  } catch (error: unknown) {
120
167
  console.error("Failed to generate tree snapshot:", (error as Error).message);
@@ -6,13 +6,17 @@ import {
6
6
  ResponseError,
7
7
  } from "./generated/runtime.ts";
8
8
  import * as apis from "./generated/apis/index.ts";
9
- import { simplifyName } from "./utils.ts";
10
9
  import { TokenCache } from "./tokenCache.ts";
11
10
 
12
11
  // Re-export all model types from generated/models
13
12
  export * from "./generated/models";
14
13
 
15
- // Rename the interface to avoid naming conflict if needed
14
+ const simplifyName = (name: string): string =>
15
+ name
16
+ .replace(/ById$/, "")
17
+ .replace(/Api$/, "")
18
+ .replace(/^(\w)/, (_, c) => c.toLowerCase());
19
+
16
20
  export interface QuickbaseClient extends IQuickbaseClient {}
17
21
 
18
22
  export interface QuickbaseConfig {
@@ -51,19 +55,18 @@ const getParamNames = (fn: (...args: any[]) => any): string[] =>
51
55
  .map((p) => p.trim().split("=")[0].trim())
52
56
  .filter((p) => p && !p.match(/^\{/) && p !== "options");
53
57
 
54
- // New utility function to extract dbid
55
58
  const extractDbid = (
56
59
  params: Partial<TempTokenParams>,
57
60
  errorMessage: string
58
61
  ): string => {
59
- const dbid = params.appId || params.tableId || params.dbid;
62
+ const dbid = params.dbid || params.tableId || params.appId;
60
63
  if (!dbid) {
61
64
  throw new Error(errorMessage);
62
65
  }
63
66
  return dbid;
64
67
  };
65
68
 
66
- export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
69
+ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
67
70
  const {
68
71
  realm,
69
72
  userToken,
@@ -134,15 +137,13 @@ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
134
137
  rawMethodName
135
138
  ) as keyof QuickbaseClient;
136
139
  const method = api[rawMethodName as keyof typeof api];
137
- const boundMethod = method.bind(api) as unknown;
140
+ const boundMethod = method.bind(api as any) as unknown;
138
141
  if (typeof boundMethod === "function" && boundMethod.length <= 2) {
139
142
  methodMap[simplifiedName] = {
140
143
  api,
141
144
  method: boundMethod as ApiMethod<typeof simplifiedName>,
142
145
  paramMap: getParamNames(method),
143
146
  };
144
- if (debug)
145
- console.log(`Mapped ${rawMethodName} to ${simplifiedName}`);
146
147
  }
147
148
  });
148
149
  }
@@ -200,13 +201,17 @@ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
200
201
  ): Promise<ReturnType<QuickbaseClient[K]>> => {
201
202
  const methodInfo = methodMap[methodName];
202
203
  if (!methodInfo) {
203
- console.error(`Method ${methodName} not found in methodMap`, methodMap);
204
204
  throw new Error(`Method ${methodName} not found`);
205
205
  }
206
206
 
207
+ const args: [any, RequestInit | undefined] =
208
+ methodInfo.paramMap.length === 1 &&
209
+ methodInfo.paramMap[0] === "requestParameters"
210
+ ? [{ requestParameters: params }, undefined]
211
+ : [params, undefined];
212
+
207
213
  let token =
208
214
  initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
209
- let initOverrides: RequestInit = {};
210
215
 
211
216
  if (methodName === "getTempTokenDBID" && useTempTokens) {
212
217
  const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
@@ -223,12 +228,9 @@ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
223
228
  params,
224
229
  `No dbid found in params for ${methodName} to fetch temp token`
225
230
  );
226
- if (debug)
227
- console.log(`Cache state before fetch for ${dbid}:`, tokenCache.dump());
228
231
  const cachedToken = tokenCache.get(dbid);
229
232
  if (cachedToken) {
230
233
  token = cachedToken;
231
- if (debug) console.log(`Reusing cached token for dbid: ${dbid}`, token);
232
234
  } else {
233
235
  if (typeof globalThis.window === "undefined" && !fetchApi) {
234
236
  throw new Error(
@@ -242,24 +244,16 @@ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
242
244
  >;
243
245
  }
244
246
  }
245
- initOverrides.headers = {
246
- ...headers,
247
- Authorization: `QB-TEMP-TOKEN ${token}`,
247
+ args[1] = {
248
+ headers: {
249
+ ...headers,
250
+ Authorization: `QB-TEMP-TOKEN ${token}`,
251
+ },
248
252
  };
249
253
  }
250
254
 
251
- if (debug) console.log(`Invoking ${methodName} with params:`, params);
252
- if (debug)
253
- console.log(`Calling method with args:`, [params, initOverrides]);
254
- const args: [any, RequestInit | undefined] =
255
- methodInfo.paramMap.length === 1 &&
256
- methodInfo.paramMap[0] === "requestParameters"
257
- ? [params, initOverrides]
258
- : [params, initOverrides];
259
-
260
255
  try {
261
256
  const response = await methodInfo.method(...args);
262
- if (debug) console.log(`Response from ${methodName}:`, response);
263
257
  return response;
264
258
  } catch (error) {
265
259
  if (
@@ -267,34 +261,38 @@ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
267
261
  error.response.status === 401 &&
268
262
  retryCount < 1
269
263
  ) {
270
- if (debug)
264
+ if (debug) {
271
265
  console.log(
272
266
  `Authorization error for ${methodName}, refreshing token:`,
273
267
  error.message
274
268
  );
269
+ }
275
270
  const dbid = extractDbid(
276
271
  params,
277
272
  `No dbid to refresh token after authorization error`
278
273
  );
279
274
  token = await fetchTempToken(dbid);
280
- initOverrides.headers = {
281
- ...headers,
282
- Authorization: `QB-TEMP-TOKEN ${token}`,
275
+ args[1] = {
276
+ headers: {
277
+ ...headers,
278
+ Authorization: `QB-TEMP-TOKEN ${token}`,
279
+ },
283
280
  };
284
- if (debug) console.log(`Retrying ${methodName} with new token`);
281
+ if (debug) {
282
+ console.log(`Retrying ${methodName} with new token`);
283
+ }
285
284
  return invokeMethod(methodName, params, retryCount + 1);
286
285
  }
287
286
  if (error instanceof ResponseError) {
288
287
  let errorMessage = error.message;
289
288
  try {
290
289
  const errorBody: { message?: string } = await error.response.json();
291
- console.log(`Error response body for ${methodName}:`, errorBody);
290
+ if (debug) {
291
+ console.log(`Error response body for ${methodName}:`, errorBody);
292
+ }
292
293
  errorMessage = errorBody.message || errorMessage;
293
294
  } catch (e) {
294
- console.log(
295
- `Failed to parse error response for ${methodName}:`,
296
- error.message
297
- );
295
+ // Silent fail on parse error
298
296
  }
299
297
  throw new Error(
300
298
  `API Error: ${errorMessage} (Status: ${error.response.status})`
@@ -304,17 +302,16 @@ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
304
302
  }
305
303
  };
306
304
 
307
- return new Proxy<QuickbaseClient>({} as QuickbaseClient, {
305
+ const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, {
308
306
  get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
309
307
  if (prop in methodMap) {
310
308
  const methodName = prop as keyof QuickbaseClient;
311
- return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) => {
312
- if (debug) console.log(`Proxy called ${methodName} with:`, params);
313
- return invokeMethod(methodName, params);
314
- };
309
+ return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
310
+ invokeMethod(methodName, params);
315
311
  }
316
- console.warn(`Method ${prop} not found in methodMap`);
317
312
  return undefined;
318
313
  },
319
314
  });
315
+
316
+ return proxy;
320
317
  }