quickbase-js 0.1.0-beta.7 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +1292 -125
  4. package/dist/esm/quickbase.js +666 -190
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +674 -189
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/open-api/fix-spec-main.ts +69 -68
  13. package/open-api/generate-override-report.ts +214 -0
  14. package/open-api/generate-unified-interface.ts +162 -103
  15. package/open-api/output/override-report.html +11 -0
  16. package/open-api/output/override-report.json +14395 -58
  17. package/open-api/output/quickbase-fixed.json +583 -2
  18. package/open-api/schema/tags/fields.ts +331 -16
  19. package/open-api/schema/tags/index.ts +2 -4
  20. package/open-api/types/spec.ts +2 -1
  21. package/open-api/utils/generateJsDoc.ts +96 -0
  22. package/package.json +33 -33
  23. package/src/RateLimitError.ts +12 -0
  24. package/src/ThrottleBucket.ts +41 -0
  25. package/src/authorizationStrategy.ts +246 -0
  26. package/src/generated-unified/QuickbaseClient.ts +809 -119
  27. package/src/invokeMethod.ts +242 -0
  28. package/src/quickbaseClient.ts +117 -248
  29. package/src/rateLimiter.ts +25 -0
  30. package/src/tokenCache.ts +10 -26
  31. package/tests/setup.ts +2 -0
  32. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  33. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  34. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  35. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  36. package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
  37. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  38. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  39. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  40. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  41. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  42. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  43. package/tests/vitest/qb/index.ts +1 -0
  44. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  45. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  46. package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
  47. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  48. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  49. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  50. package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
  51. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
  52. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  53. package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
  54. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  55. package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
  56. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  57. package/tests/vitest/unit/fields/createField.test.ts +180 -120
  58. package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
  59. package/tests/vitest/unit/fields/updateField.test.ts +285 -0
  60. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  61. package/tests/vitest/unit/index.ts +2 -1
  62. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  63. package/tests/vitest/unit/rateLimit.test.ts +191 -0
  64. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  65. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  66. package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
  67. package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
  68. package/tools/log-gen.ts +20 -7
  69. package/tools/log-gen.yaml +3446 -1848
  70. package/tsconfig.json +5 -2
  71. package/vitest.config.ts +2 -2
  72. package/notes.txt +0 -158
  73. package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
  74. package/tests/vitest/unit/auth/two401s.test.ts +0 -86
  75. package/tools/generate-types.ts +0 -29
  76. package/tools/openapi-schema-validator.ts +0 -38
  77. package/tools/spec-analyzer.ts +0 -163
  78. package/tools/split-spec.ts +0 -148
  79. package/tools/test-import.ts +0 -3
  80. package/tools/validate-spec.ts +0 -40
  81. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  82. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -1,4 +1,5 @@
1
- // open-api/generate-unified-interface.ts
1
+ #!/usr/bin/env node
2
+ console.log("Script started");
2
3
 
3
4
  import {
4
5
  readFileSync,
@@ -7,12 +8,27 @@ import {
7
8
  existsSync,
8
9
  readdirSync,
9
10
  } from "fs";
11
+ console.log("FS modules imported");
12
+
10
13
  import { join, dirname } from "path";
14
+ console.log("Path modules imported");
15
+
11
16
  import { fileURLToPath } from "url";
17
+ console.log("URL module imported");
18
+
12
19
  import { OpenAPIV2 } from "openapi-types";
13
- import { simplifyName } from "../src/utils.ts";
20
+ console.log("openapi-types imported");
21
+
22
+ import { Project } from "ts-morph";
23
+ console.log("ts-morph imported");
24
+
25
+ import { generateJsDoc } from "./utils/generateJsDoc.js";
26
+
27
+ const simplifyName = (str: string): string => str;
14
28
 
15
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ console.log("__dirname set:", __dirname);
31
+
16
32
  const SPEC_FILE = join(__dirname, "output", "quickbase-fixed.json");
17
33
  const OUTPUT_DIR = join(__dirname, "..", "src", "generated-unified");
18
34
  const OUTPUT_FILE = join(OUTPUT_DIR, "QuickbaseClient.ts");
@@ -52,30 +68,18 @@ function mapRefToType(
52
68
  const refParts = schema.$ref.split("/");
53
69
  const model = refParts[refParts.length - 1];
54
70
  const pascalModel = model.charAt(0).toUpperCase() + model.slice(1);
55
- const camelModel = model.charAt(0).toLowerCase() + model.slice(1);
56
71
  if (availableModels.includes(pascalModel)) {
57
72
  modelImports.add(pascalModel);
58
73
  return pascalModel;
59
- } else if (availableModels.includes(camelModel)) {
60
- modelImports.add(camelModel);
61
- return camelModel;
62
74
  }
63
75
  missingTypes.add(pascalModel);
64
- console.warn(
65
- `Type ${pascalModel} not found in /generated/models, defaulting to 'any'`
66
- );
76
+ console.warn(`Type ${pascalModel} not found, defaulting to 'any'`);
67
77
  return "any";
68
78
  }
69
79
 
70
80
  if ("type" in schema) {
71
81
  if (schema.type === "object" && schema.properties) {
72
82
  const props = schema.properties;
73
- if (props.data && props.data.format === "binary") {
74
- return "ArrayBuffer"; // Map binary object to ArrayBuffer
75
- }
76
- if (props.content && props.content.format === "yaml") {
77
- return "string"; // Map YAML object to string
78
- }
79
83
  const propTypes = Object.entries(props).map(([key, prop]) => {
80
84
  const propSchema = prop as OpenAPIV2.SchemaObject;
81
85
  const propType = mapRefToType(
@@ -104,31 +108,64 @@ function mapRefToType(
104
108
  );
105
109
  return `${itemType}[]`;
106
110
  }
107
- if (schema.type === "object" && schema.additionalProperties) {
108
- const additionalProps = schema.additionalProperties as
109
- | OpenAPIV2.SchemaObject
110
- | OpenAPIV2.ReferenceObject
111
- | boolean;
112
- if (typeof additionalProps === "boolean") {
113
- return "{ [key: string]: any }";
114
- }
115
- const valueType = mapRefToType(
116
- additionalProps,
117
- modelImports,
118
- spec,
119
- depth + 1,
120
- availableModels,
121
- missingTypes
122
- );
123
- return `{ [key: string]: ${valueType} }`;
124
- }
125
111
  return mapOpenApiTypeToTs(schema.type);
126
112
  }
127
113
 
128
114
  return "any";
129
115
  }
130
116
 
131
- function generateInterface() {
117
+ interface PropertyDetail {
118
+ name: string;
119
+ type: string;
120
+ required: boolean;
121
+ jsdoc?: string;
122
+ }
123
+
124
+ function parseInterfaceProperties(
125
+ modelName: string,
126
+ modelsDir: string
127
+ ): PropertyDetail[] {
128
+ const project = new Project();
129
+ const filePath = join(modelsDir, `${modelName}.ts`);
130
+ if (!existsSync(filePath)) {
131
+ console.warn(`Model file ${filePath} not found for ${modelName}`);
132
+ return [];
133
+ }
134
+
135
+ const sourceFile = project.addSourceFileAtPath(filePath);
136
+ const interfaceDec = sourceFile.getInterface(modelName);
137
+ if (!interfaceDec) {
138
+ console.warn(`Interface ${modelName} not found in ${filePath}`);
139
+ return [];
140
+ }
141
+
142
+ return interfaceDec.getProperties().map((prop) => {
143
+ const jsDocs = prop.getJsDocs();
144
+ const jsdocText =
145
+ jsDocs.length > 0 ? jsDocs[0].getDescription().trim() : undefined;
146
+ return {
147
+ name: prop.getName(),
148
+ type: prop.getType().getText(prop),
149
+ required: !prop.hasQuestionToken(),
150
+ jsdoc: jsdocText,
151
+ };
152
+ });
153
+ }
154
+
155
+ interface ParamDetail {
156
+ name: string;
157
+ type: string;
158
+ required: boolean;
159
+ properties: PropertyDetail[];
160
+ }
161
+
162
+ function generateInterface(includeResponseProperties: boolean = false): void {
163
+ console.log(
164
+ "Generating interface with includeResponseProperties:",
165
+ includeResponseProperties
166
+ );
167
+
168
+ console.log("Checking spec file:", SPEC_FILE);
132
169
  if (!existsSync(SPEC_FILE)) {
133
170
  console.error(
134
171
  `Spec file ${SPEC_FILE} not found. Run 'npm run fix-spec' first.`
@@ -136,12 +173,24 @@ function generateInterface() {
136
173
  process.exit(1);
137
174
  }
138
175
 
139
- console.log("Generating unified QuickbaseClient interface...");
140
- const spec = JSON.parse(
141
- readFileSync(SPEC_FILE, "utf8")
142
- ) as OpenAPIV2.Document;
176
+ console.log("Reading spec file...");
177
+ const specContent = readFileSync(SPEC_FILE, "utf8");
178
+ const spec: OpenAPIV2.Document = JSON.parse(specContent);
179
+ console.log("Spec parsed, keys:", Object.keys(spec));
180
+
143
181
  const { paths } = spec;
182
+ if (!paths) {
183
+ console.error("No 'paths' in spec");
184
+ process.exit(1);
185
+ }
144
186
 
187
+ console.log("Checking models directory:", MODELS_DIR);
188
+ if (!existsSync(MODELS_DIR)) {
189
+ console.error(
190
+ `Models directory ${MODELS_DIR} not found. Run 'npm run gen:openapi' first.`
191
+ );
192
+ process.exit(1);
193
+ }
145
194
  const availableModels = readdirSync(MODELS_DIR)
146
195
  .filter((file) => file.endsWith(".ts") && !file.startsWith("index"))
147
196
  .map((file) => file.replace(".ts", ""));
@@ -155,7 +204,6 @@ function generateInterface() {
155
204
  paths as OpenAPIV2.PathsObject
156
205
  )) {
157
206
  if (!methodsObj) continue;
158
-
159
207
  for (const [method, operation] of Object.entries(
160
208
  methodsObj as OpenAPIV2.PathItemObject
161
209
  )) {
@@ -163,71 +211,89 @@ function generateInterface() {
163
211
  if (!op || !op.operationId) continue;
164
212
 
165
213
  const opId = simplifyName(op.operationId);
166
- const summary = op.summary || "No description available.";
167
- const params = (op.parameters || [])
168
- .filter((p) => {
169
- const param = p as OpenAPIV2.Parameter;
214
+ const summary = op.summary || "No description.";
215
+ const docLink = `https://developer.quickbase.com/operation/${op.operationId}`;
216
+
217
+ const paramDetails: ParamDetail[] = (op.parameters || [])
218
+ .filter((p: OpenAPIV2.ParameterObject | OpenAPIV2.ReferenceObject) => {
219
+ const param = p as OpenAPIV2.ParameterObject;
170
220
  return !["QB-Realm-Hostname", "Authorization", "User-Agent"].includes(
171
- "name" in param ? param.name : ""
221
+ param.name || ""
172
222
  );
173
223
  })
174
- .map((param: OpenAPIV2.Parameter) => {
175
- if (!("name" in param)) return "";
176
- let type: string;
177
- if ("schema" in param && param.schema) {
224
+ .map((param: OpenAPIV2.ParameterObject | OpenAPIV2.ReferenceObject) => {
225
+ const p = param as OpenAPIV2.ParameterObject;
226
+ if (!p.name) return null;
227
+ let type = "any";
228
+ let properties: PropertyDetail[] = [];
229
+ if ("schema" in p && p.schema) {
178
230
  type = mapRefToType(
179
- param.schema,
231
+ p.schema,
180
232
  modelImports,
181
233
  spec,
182
234
  1,
183
235
  availableModels,
184
236
  missingTypes
185
237
  );
186
- } else if ("type" in param && param.type) {
187
- type = mapOpenApiTypeToTs(param.type);
188
- } else {
189
- console.warn(
190
- `Parameter ${param.name} in ${opId} has no type or schema, defaulting to 'any'`
191
- );
192
- type = "any";
238
+ if ("$ref" in p.schema && p.schema.$ref) {
239
+ const refParts = p.schema.$ref.split("/");
240
+ const model = refParts[refParts.length - 1];
241
+ const pascalModel =
242
+ model.charAt(0).toUpperCase() + model.slice(1);
243
+ properties = parseInterfaceProperties(pascalModel, MODELS_DIR);
244
+ }
245
+ } else if ("type" in p && p.type) {
246
+ type = mapOpenApiTypeToTs(p.type);
193
247
  }
194
- const paramName = param.in === "body" ? "body" : param.name;
195
- return `${paramName}${param.required ? "" : "?"}: ${type}`;
248
+ const paramName = p.in === "body" ? "body" : p.name;
249
+ return {
250
+ name: paramName,
251
+ type,
252
+ required: p.required || false,
253
+ properties,
254
+ };
196
255
  })
197
- .filter((param) => param !== "")
256
+ .filter((p): p is ParamDetail => p !== null);
257
+
258
+ const params = paramDetails
259
+ .map((p) => `${p.name}${p.required ? "" : "?"}: ${p.type}`)
198
260
  .join("; ");
199
261
 
200
- const successResponses = ["200", "207"]
201
- .map((code) => ({
202
- code,
203
- response: op.responses?.[code] as
204
- | OpenAPIV2.ResponseObject
205
- | undefined,
206
- }))
207
- .filter(({ response }) => response?.schema);
208
- const returnTypes = successResponses.map(({ response }) =>
209
- mapRefToType(
210
- response!.schema,
211
- modelImports,
212
- spec,
213
- 1,
214
- availableModels,
215
- missingTypes
262
+ const returnTypes = ["200", "207"]
263
+ .map(
264
+ (code) => op.responses?.[code] as OpenAPIV2.ResponseObject | undefined
216
265
  )
217
- );
218
- const uniqueReturnTypes = [...new Set(returnTypes)];
266
+ .filter((response) => response?.schema)
267
+ .map((response) =>
268
+ mapRefToType(
269
+ response!.schema,
270
+ modelImports,
271
+ spec,
272
+ 1,
273
+ availableModels,
274
+ missingTypes
275
+ )
276
+ );
219
277
  const returnType =
220
- uniqueReturnTypes.length > 1
221
- ? uniqueReturnTypes.join(" | ")
222
- : uniqueReturnTypes[0] || "void";
223
-
224
- const jsDoc = [
225
- ` /**`,
226
- ` * ${summary}`,
227
- ` * @param params - Parameters for the ${opId} operation`,
228
- ` * @returns A promise resolving to the ${opId} response`,
229
- ` */`,
230
- ].join("\n");
278
+ returnTypes.length > 1
279
+ ? returnTypes.join(" | ")
280
+ : returnTypes[0] || "void";
281
+
282
+ const returnTypeDetails: PropertyDetail[] =
283
+ includeResponseProperties &&
284
+ returnTypes.length === 1 &&
285
+ returnTypes[0] !== "void"
286
+ ? parseInterfaceProperties(returnTypes[0], MODELS_DIR)
287
+ : [];
288
+
289
+ const jsDoc = generateJsDoc({
290
+ summary,
291
+ opId,
292
+ paramDetails,
293
+ returnType,
294
+ returnTypeDetails,
295
+ docLink,
296
+ });
231
297
 
232
298
  methods.push(
233
299
  `${jsDoc}\n ${opId}: (params: { ${params} }) => Promise<${returnType}>;`
@@ -235,20 +301,11 @@ function generateInterface() {
235
301
  }
236
302
  }
237
303
 
238
- // Always write the missing-types-report.json to reflect the current state
239
- if (missingTypes.size > 0) {
240
- console.log(
241
- "Missing types detected (defaulted to 'any'):",
242
- Array.from(missingTypes)
243
- );
244
- } else {
245
- console.log("No missing types detected.");
246
- }
304
+ console.log("Writing missing types report...");
247
305
  writeFileSync(
248
306
  join(OUTPUT_DIR, "missing-types-report.json"),
249
307
  JSON.stringify({ missingTypes: Array.from(missingTypes) }, null, 2)
250
308
  );
251
- console.log("Missing types report saved to missing-types-report.json");
252
309
 
253
310
  const importStatement =
254
311
  modelImports.size > 0
@@ -256,22 +313,24 @@ function generateInterface() {
256
313
  .sort()
257
314
  .join(", ")} } from "../generated/models";`
258
315
  : "";
259
- console.log(`Imports: ${Array.from(modelImports).join(", ")}`);
260
-
261
316
  const interfaceContent = `// Generated on ${new Date().toISOString()}\n${importStatement}\n\nexport interface QuickbaseClient {\n${methods.join(
262
317
  "\n"
263
318
  )}\n}\n`;
264
319
 
320
+ console.log("Ensuring output directory exists...");
265
321
  if (!existsSync(OUTPUT_DIR)) {
266
322
  mkdirSync(OUTPUT_DIR, { recursive: true });
267
323
  }
268
324
  writeFileSync(OUTPUT_FILE, interfaceContent, "utf8");
269
- console.log(`Generated ${OUTPUT_FILE}`);
325
+ console.log("Generated:", OUTPUT_FILE);
270
326
  }
271
327
 
328
+ console.log("Entering try block");
272
329
  try {
273
- generateInterface();
330
+ generateInterface(true); // Toggle response properties here
274
331
  } catch (error) {
275
- console.error("Generation failed:", error);
332
+ console.error("Error in generateInterface:", error);
276
333
  process.exit(1);
277
334
  }
335
+
336
+ console.log("Script completed");