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.
- package/CHANGELOG.md +55 -0
- package/README.md +46 -130
- package/dist/esm/quickbase.d.ts +1292 -125
- package/dist/esm/quickbase.js +666 -190
- 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 +674 -189
- 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 +162 -103
- 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/open-api/utils/generateJsDoc.ts +96 -0
- package/package.json +33 -33
- package/src/RateLimitError.ts +12 -0
- package/src/ThrottleBucket.ts +41 -0
- package/src/authorizationStrategy.ts +246 -0
- package/src/generated-unified/QuickbaseClient.ts +809 -119
- package/src/invokeMethod.ts +242 -0
- package/src/quickbaseClient.ts +117 -248
- package/src/rateLimiter.ts +25 -0
- package/src/tokenCache.ts +10 -26
- 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/mockQuickbaseServer.ts +60 -0
- package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
- package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -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/getFieldUsage.test.ts +58 -150
- package/tests/vitest/qb/fields/updateField.test.ts +93 -0
- package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
- package/tests/vitest/qb/index.ts +1 -0
- package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
- package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
- package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
- package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
- package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
- package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
- package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
- package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
- package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
- package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
- package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
- package/tests/vitest/unit/fields/createField.test.ts +180 -120
- package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
- package/tests/vitest/unit/fields/updateField.test.ts +285 -0
- package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
- package/tests/vitest/unit/index.ts +2 -1
- package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
- package/tests/vitest/unit/rateLimit.test.ts +191 -0
- package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
- package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
- package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
- package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
- package/tools/log-gen.ts +20 -7
- package/tools/log-gen.yaml +3446 -1848
- package/tsconfig.json +5 -2
- package/vitest.config.ts +2 -2
- package/notes.txt +0 -158
- package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
- package/tests/vitest/unit/auth/two401s.test.ts +0 -86
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
221
|
+
param.name || ""
|
|
172
222
|
);
|
|
173
223
|
})
|
|
174
|
-
.map((param: OpenAPIV2.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
231
|
+
p.schema,
|
|
180
232
|
modelImports,
|
|
181
233
|
spec,
|
|
182
234
|
1,
|
|
183
235
|
availableModels,
|
|
184
236
|
missingTypes
|
|
185
237
|
);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 =
|
|
195
|
-
return
|
|
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((
|
|
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
|
|
201
|
-
.map(
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
?
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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(
|
|
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("
|
|
332
|
+
console.error("Error in generateInterface:", error);
|
|
276
333
|
process.exit(1);
|
|
277
334
|
}
|
|
335
|
+
|
|
336
|
+
console.log("Script completed");
|