@tzurp/openapi-skills 1.2.9 → 1.3.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 (80) hide show
  1. package/README.md +67 -19
  2. package/dist/cli.min.js +71 -0
  3. package/package.json +60 -64
  4. package/skill-templates/SKILL.md +30 -16
  5. package/bin/openapi-skills +0 -4
  6. package/dist/cli.d.ts +0 -37
  7. package/dist/cli.js +0 -1646
  8. package/dist/cli.js.map +0 -1
  9. package/dist/client-schema-builder.d.ts +0 -35
  10. package/dist/client-schema-builder.js +0 -268
  11. package/dist/client-schema-builder.js.map +0 -1
  12. package/dist/helper/base64url-json.d.ts +0 -3
  13. package/dist/helper/base64url-json.js +0 -28
  14. package/dist/helper/base64url-json.js.map +0 -1
  15. package/dist/helper/dotNotation.d.ts +0 -3
  16. package/dist/helper/dotNotation.js +0 -34
  17. package/dist/helper/dotNotation.js.map +0 -1
  18. package/dist/helper/endpoint-filter.d.ts +0 -16
  19. package/dist/helper/endpoint-filter.js +0 -165
  20. package/dist/helper/endpoint-filter.js.map +0 -1
  21. package/dist/helper/endpoint-utils.d.ts +0 -2
  22. package/dist/helper/endpoint-utils.js +0 -10
  23. package/dist/helper/endpoint-utils.js.map +0 -1
  24. package/dist/helper/error-codes.d.ts +0 -27
  25. package/dist/helper/error-codes.js +0 -116
  26. package/dist/helper/error-codes.js.map +0 -1
  27. package/dist/helper/error-formatter.d.ts +0 -46
  28. package/dist/helper/error-formatter.js +0 -72
  29. package/dist/helper/error-formatter.js.map +0 -1
  30. package/dist/helper/get-operation-filter.d.ts +0 -17
  31. package/dist/helper/get-operation-filter.js +0 -26
  32. package/dist/helper/get-operation-filter.js.map +0 -1
  33. package/dist/helper/graphql.d.ts +0 -40
  34. package/dist/helper/graphql.js +0 -967
  35. package/dist/helper/graphql.js.map +0 -1
  36. package/dist/helper/http-client.d.ts +0 -28
  37. package/dist/helper/http-client.js +0 -90
  38. package/dist/helper/http-client.js.map +0 -1
  39. package/dist/helper/json-updater.d.ts +0 -17
  40. package/dist/helper/json-updater.js +0 -195
  41. package/dist/helper/json-updater.js.map +0 -1
  42. package/dist/helper/logger.d.ts +0 -13
  43. package/dist/helper/logger.js +0 -56
  44. package/dist/helper/logger.js.map +0 -1
  45. package/dist/helper/output-size.d.ts +0 -5
  46. package/dist/helper/output-size.js +0 -7
  47. package/dist/helper/output-size.js.map +0 -1
  48. package/dist/helper/parameter-schema.d.ts +0 -14
  49. package/dist/helper/parameter-schema.js +0 -36
  50. package/dist/helper/parameter-schema.js.map +0 -1
  51. package/dist/helper/paths.d.ts +0 -11
  52. package/dist/helper/paths.js +0 -47
  53. package/dist/helper/paths.js.map +0 -1
  54. package/dist/helper/prompt-delete.d.ts +0 -1
  55. package/dist/helper/prompt-delete.js +0 -13
  56. package/dist/helper/prompt-delete.js.map +0 -1
  57. package/dist/helper/request-artifacts.d.ts +0 -14
  58. package/dist/helper/request-artifacts.js +0 -60
  59. package/dist/helper/request-artifacts.js.map +0 -1
  60. package/dist/helper/request-preparation.d.ts +0 -23
  61. package/dist/helper/request-preparation.js +0 -85
  62. package/dist/helper/request-preparation.js.map +0 -1
  63. package/dist/helper/sanitizer.d.ts +0 -1
  64. package/dist/helper/sanitizer.js +0 -17
  65. package/dist/helper/sanitizer.js.map +0 -1
  66. package/dist/helper/update-check.d.ts +0 -1
  67. package/dist/helper/update-check.js +0 -36
  68. package/dist/helper/update-check.js.map +0 -1
  69. package/dist/index.d.ts +0 -50
  70. package/dist/index.js +0 -163
  71. package/dist/index.js.map +0 -1
  72. package/dist/install-skill.d.ts +0 -5
  73. package/dist/install-skill.js +0 -139
  74. package/dist/install-skill.js.map +0 -1
  75. package/dist/parser.d.ts +0 -12
  76. package/dist/parser.js +0 -393
  77. package/dist/parser.js.map +0 -1
  78. package/dist/validate-response.d.ts +0 -47
  79. package/dist/validate-response.js +0 -1017
  80. package/dist/validate-response.js.map +0 -1
@@ -1,1017 +0,0 @@
1
- import { FetchClient } from "./helper/http-client.js";
2
- import fs from "fs-extra";
3
- import path from "path";
4
- import { getSchemaPath, getSchemasDir, findRequestResponseDir } from "./helper/paths.js";
5
- import Ajv from "ajv";
6
- import { buildClientCodeSchema } from "./client-schema-builder.js";
7
- import { DELETE_SENTINEL, loadJsonObject, updateJsonFile } from "./helper/json-updater.js";
8
- import { loadConfig } from "./index.js";
9
- import getSanitizedOperationId from "./helper/endpoint-utils.js";
10
- import { getParameterDefaultValue } from "./helper/parameter-schema.js";
11
- import { getByPath } from "./helper/dotNotation.js";
12
- import { buildGraphQLArtifact, findGraphQLEndpoint } from "./helper/graphql.js";
13
- import { getEndpointsPath } from "./helper/paths.js";
14
- import { ensureEndpointSchemaFile } from "./parser.js";
15
- import { fileURLToPath } from "url";
16
- function flattenToDotNotation(value, prefix = "", out = {}) {
17
- if (!value || typeof value !== "object" || Array.isArray(value)) {
18
- return out;
19
- }
20
- for (const [key, child] of Object.entries(value)) {
21
- const path = prefix ? `${prefix}.${key}` : key;
22
- if (child && typeof child === "object" && !Array.isArray(child)) {
23
- flattenToDotNotation(child, path, out);
24
- }
25
- else {
26
- out[path] = child;
27
- }
28
- }
29
- return out;
30
- }
31
- function collectNestedUpdateKeys(value, prefix = "") {
32
- if (!value || typeof value !== "object" || Array.isArray(value)) {
33
- return [];
34
- }
35
- const nestedKeys = [];
36
- for (const [key, childValue] of Object.entries(value)) {
37
- const currentKey = prefix ? `${prefix}.${key}` : key;
38
- if (childValue && typeof childValue === "object" && !Array.isArray(childValue)) {
39
- nestedKeys.push(currentKey);
40
- nestedKeys.push(...collectNestedUpdateKeys(childValue, currentKey));
41
- }
42
- }
43
- return Array.from(new Set(nestedKeys));
44
- }
45
- function buildUpdateRequestWarning(requestJsonUpdates) {
46
- const nestedKeys = collectNestedUpdateKeys(requestJsonUpdates);
47
- if (nestedKeys.length === 0) {
48
- return undefined;
49
- }
50
- const flattened = flattenToDotNotation(requestJsonUpdates);
51
- const example = JSON.stringify(flattened);
52
- return [
53
- `Use only flattened object dot-notation keys with --force.`,
54
- `Example: --force --update-request '${example}'.`,
55
- `Detected nested keys: ${nestedKeys.join(", ")}.`
56
- ].join(" ");
57
- }
58
- function describeValueType(value) {
59
- if (value === null) {
60
- return "null";
61
- }
62
- if (isFileDescriptorTemplateValue(value)) {
63
- return "file";
64
- }
65
- if (Array.isArray(value)) {
66
- return "array";
67
- }
68
- return typeof value;
69
- }
70
- function isFileDescriptorTemplateValue(value) {
71
- return !!value
72
- && typeof value === "object"
73
- && !Array.isArray(value)
74
- && value.kind === "file";
75
- }
76
- function isFileDescriptorValue(value) {
77
- return !!value
78
- && typeof value === "object"
79
- && !Array.isArray(value)
80
- && typeof value.path === "string"
81
- && (value.kind === undefined || value.kind === "file");
82
- }
83
- function matchesExpectedTemplateValue(expectedValue, actualValue) {
84
- if (isFileDescriptorTemplateValue(expectedValue)) {
85
- return typeof actualValue === "string"
86
- || isFileDescriptorValue(actualValue);
87
- }
88
- if (expectedValue === null) {
89
- return actualValue === null;
90
- }
91
- if (Array.isArray(expectedValue)) {
92
- return Array.isArray(actualValue);
93
- }
94
- if (typeof expectedValue === "object") {
95
- return !!actualValue && typeof actualValue === "object" && !Array.isArray(actualValue);
96
- }
97
- return typeof actualValue === typeof expectedValue;
98
- }
99
- function isGraphQLRequestJson(value) {
100
- return !!value && typeof value === "object" && !Array.isArray(value)
101
- && typeof value.query === "string"
102
- && typeof value.variables === "object"
103
- && !Array.isArray(value.variables);
104
- }
105
- function normalizeMultipartContentType(value) {
106
- if (!value) {
107
- return undefined;
108
- }
109
- return value.split(",")[0]?.trim() || undefined;
110
- }
111
- function inferMimeTypeFromExtension(filePath) {
112
- const extension = path.extname(filePath).toLowerCase();
113
- switch (extension) {
114
- case ".pdf":
115
- return "application/pdf";
116
- case ".jpg":
117
- case ".jpeg":
118
- return "image/jpeg";
119
- case ".png":
120
- return "image/png";
121
- case ".webp":
122
- return "image/webp";
123
- case ".gif":
124
- return "image/gif";
125
- case ".txt":
126
- return "text/plain";
127
- case ".csv":
128
- return "text/csv";
129
- case ".json":
130
- return "application/json";
131
- case ".xml":
132
- return "application/xml";
133
- case ".xlsx":
134
- return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
135
- case ".xls":
136
- return "application/vnd.ms-excel";
137
- case ".docx":
138
- return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
139
- case ".zip":
140
- return "application/zip";
141
- default:
142
- return "application/octet-stream";
143
- }
144
- }
145
- function normalizeFilePath(value) {
146
- const trimmed = value.trim();
147
- if (trimmed.startsWith("@")) {
148
- return path.resolve(trimmed.slice(1));
149
- }
150
- if (trimmed.startsWith("file://")) {
151
- return path.resolve(fileURLToPath(new URL(trimmed)));
152
- }
153
- return path.resolve(trimmed);
154
- }
155
- async function resolveFileInput(value, fallbackMimeType) {
156
- let filePath;
157
- let fileName;
158
- let mimeType;
159
- if (typeof value === "string") {
160
- if (value.trim().length === 0) {
161
- throw new Error("File upload path cannot be empty.");
162
- }
163
- filePath = normalizeFilePath(value);
164
- }
165
- else if (isFileDescriptorValue(value)) {
166
- if (value.path.trim().length === 0) {
167
- throw new Error("File upload path cannot be empty.");
168
- }
169
- filePath = normalizeFilePath(value.path);
170
- fileName = value.fileName?.trim() || undefined;
171
- mimeType = value.mimeType?.trim() || undefined;
172
- }
173
- if (!filePath) {
174
- throw new Error("File upload values must be a file path string or { kind: 'file', path, fileName?, mimeType? } object.");
175
- }
176
- if (!(await fs.pathExists(filePath))) {
177
- throw new Error(`Upload file not found: ${filePath}`);
178
- }
179
- const buffer = await fs.readFile(filePath);
180
- const resolvedFileName = fileName || path.basename(filePath);
181
- const resolvedMimeType = mimeType || normalizeMultipartContentType(fallbackMimeType) || inferMimeTypeFromExtension(filePath);
182
- return {
183
- buffer,
184
- path: filePath,
185
- fileName: resolvedFileName,
186
- mimeType: resolvedMimeType,
187
- };
188
- }
189
- function bufferToBlobPart(buffer) {
190
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
191
- }
192
- function findPreferredRequestContent(content) {
193
- const entries = Object.entries(content);
194
- const multipartEntry = entries.find(([contentType]) => contentType.toLowerCase().includes("multipart/form-data"));
195
- if (multipartEntry) {
196
- return { contentType: multipartEntry[0], contentObject: multipartEntry[1] };
197
- }
198
- const binaryEntry = entries.find(([contentType, contentObject]) => {
199
- const schema = contentObject?.schema;
200
- return contentType.toLowerCase().includes("octet-stream")
201
- || (schema?.type === "string" && schema?.format === "binary");
202
- });
203
- if (binaryEntry) {
204
- return { contentType: binaryEntry[0], contentObject: binaryEntry[1] };
205
- }
206
- const jsonEntry = entries.find(([contentType]) => contentType.toLowerCase().includes("json"));
207
- if (jsonEntry) {
208
- return { contentType: jsonEntry[0], contentObject: jsonEntry[1] };
209
- }
210
- const firstEntry = entries[0];
211
- return firstEntry ? { contentType: firstEntry[0], contentObject: firstEntry[1] } : undefined;
212
- }
213
- function getRequestBodyTransportSpec(operationSchema) {
214
- const requestBody = operationSchema?.requestBody;
215
- const params = Array.isArray(operationSchema?.parameters) ? operationSchema.parameters : [];
216
- const formParams = params.filter((p) => p && p.in === "formData");
217
- const consumes = Array.isArray(operationSchema?.consumes) ? operationSchema.consumes : [];
218
- const isMultipartFormData = consumes.some((contentType) => typeof contentType === "string" && contentType.toLowerCase().includes("multipart/form-data"));
219
- if (formParams.length > 0 && isMultipartFormData) {
220
- return {
221
- kind: "multipart",
222
- schema: extractRequestSchemaOAS2(operationSchema) ?? { type: "object", properties: {} },
223
- encoding: {},
224
- };
225
- }
226
- if (!requestBody || typeof requestBody !== "object") {
227
- if (formParams.length > 0) {
228
- return {
229
- kind: "multipart",
230
- schema: extractRequestSchemaOAS2(operationSchema) ?? { type: "object", properties: {} },
231
- encoding: {},
232
- };
233
- }
234
- return { kind: "json" };
235
- }
236
- const content = requestBody.content;
237
- if (content && typeof content === "object" && !Array.isArray(content)) {
238
- const preferred = findPreferredRequestContent(content);
239
- if (!preferred) {
240
- return { kind: "json" };
241
- }
242
- const mediaType = preferred.contentObject;
243
- const schema = mediaType?.schema;
244
- if (preferred.contentType.toLowerCase().includes("multipart/form-data")) {
245
- return {
246
- kind: "multipart",
247
- schema: schema && typeof schema === "object" ? schema : { type: "object", properties: {} },
248
- encoding: mediaType?.encoding ?? {},
249
- };
250
- }
251
- if (preferred.contentType.toLowerCase().includes("octet-stream") || (schema?.type === "string" && schema?.format === "binary")) {
252
- return {
253
- kind: "binary",
254
- contentType: preferred.contentType,
255
- };
256
- }
257
- }
258
- return { kind: "json" };
259
- }
260
- export async function buildRequestBodyTransport(operationSchema, requestJsonOrBody) {
261
- const transportSpec = getRequestBodyTransportSpec(operationSchema);
262
- if (transportSpec.kind === "multipart") {
263
- const requestJson = requestJsonOrBody;
264
- const hasRequestShape = !!requestJson
265
- && typeof requestJson === "object"
266
- && !Array.isArray(requestJson)
267
- && ("requestBody" in requestJson || "parameters" in requestJson);
268
- const bodyObject = hasRequestShape
269
- ? requestJson.requestBody
270
- : requestJsonOrBody;
271
- const fallbackFormData = hasRequestShape && Array.isArray(requestJson?.parameters)
272
- ? Object.fromEntries(requestJson.parameters
273
- .filter((param) => !!param
274
- && param.in === "formData"
275
- && typeof param.name === "string")
276
- .map(param => [param.name, param.value]))
277
- : {};
278
- const multipartBody = bodyObject && typeof bodyObject === "object" && !Array.isArray(bodyObject)
279
- ? bodyObject
280
- : fallbackFormData;
281
- if (!multipartBody || typeof multipartBody !== "object" || Array.isArray(multipartBody)) {
282
- throw new Error("multipart/form-data request bodies must be JSON objects.");
283
- }
284
- const formData = new FormData();
285
- const schemaProperties = transportSpec.schema?.properties && typeof transportSpec.schema.properties === "object"
286
- ? transportSpec.schema.properties
287
- : {};
288
- for (const [fieldName, fieldValue] of Object.entries(multipartBody)) {
289
- const fieldSchema = schemaProperties[fieldName];
290
- const fieldEncoding = transportSpec.encoding[fieldName];
291
- const fieldContentType = normalizeMultipartContentType(fieldEncoding?.contentType);
292
- if (fieldSchema?.type === "string" && fieldSchema?.format === "binary") {
293
- const file = await resolveFileInput(fieldValue, fieldContentType);
294
- formData.append(fieldName, new Blob([bufferToBlobPart(file.buffer)], { type: file.mimeType }), file.fileName);
295
- continue;
296
- }
297
- if (isFileDescriptorValue(fieldValue)) {
298
- const file = await resolveFileInput(fieldValue, fieldContentType);
299
- formData.append(fieldName, new Blob([bufferToBlobPart(file.buffer)], { type: file.mimeType }), file.fileName);
300
- continue;
301
- }
302
- if (fieldValue === undefined || fieldValue === null) {
303
- formData.append(fieldName, "");
304
- }
305
- else if (typeof fieldValue === "object") {
306
- formData.append(fieldName, JSON.stringify(fieldValue));
307
- }
308
- else {
309
- formData.append(fieldName, String(fieldValue));
310
- }
311
- }
312
- return { kind: "multipart", body: formData, headers: {} };
313
- }
314
- if (transportSpec.kind === "binary") {
315
- const requestJson = requestJsonOrBody;
316
- const hasRequestShape = !!requestJson
317
- && typeof requestJson === "object"
318
- && !Array.isArray(requestJson)
319
- && "requestBody" in requestJson;
320
- const binaryInput = hasRequestShape ? requestJson.requestBody : requestJsonOrBody;
321
- const file = await resolveFileInput(binaryInput, transportSpec.contentType);
322
- return {
323
- kind: "binary",
324
- body: file.buffer,
325
- headers: {
326
- "Content-Type": file.mimeType,
327
- },
328
- };
329
- }
330
- const requestJson = requestJsonOrBody;
331
- const hasRequestShape = !!requestJson
332
- && typeof requestJson === "object"
333
- && !Array.isArray(requestJson)
334
- && "requestBody" in requestJson;
335
- const jsonBody = hasRequestShape ? requestJson.requestBody : requestJsonOrBody;
336
- return {
337
- kind: "json",
338
- body: jsonBody,
339
- headers: {},
340
- };
341
- }
342
- function buildGraphQLVariables(args) {
343
- const variables = {};
344
- for (const [name, arg] of Object.entries(args)) {
345
- const normalized = arg.type.trim();
346
- const baseType = normalized.endsWith("!") ? normalized.slice(0, -1).trim() : normalized;
347
- if (baseType.startsWith("[") && baseType.endsWith("]")) {
348
- variables[name] = [];
349
- }
350
- else if (baseType === "Boolean") {
351
- variables[name] = false;
352
- }
353
- else if (baseType === "Int" || baseType === "Float") {
354
- variables[name] = 0;
355
- }
356
- else {
357
- variables[name] = "";
358
- }
359
- }
360
- return variables;
361
- }
362
- export async function getSchemaType(apiName) {
363
- const config = await loadConfig();
364
- const configuredSchemaType = config.apis?.[apiName]?.schemaType;
365
- if (configuredSchemaType === "graphql") {
366
- return "graphql";
367
- }
368
- if (configuredSchemaType === "openapi") {
369
- return "openapi";
370
- }
371
- try {
372
- const endpoints = await fs.readJson(getEndpointsPath(apiName));
373
- if (Array.isArray(endpoints) && endpoints.some((endpoint) => typeof endpoint?.rootType === "string")) {
374
- return "graphql";
375
- }
376
- }
377
- catch {
378
- }
379
- return "openapi";
380
- }
381
- async function readGraphQLBundledSource(apiName) {
382
- const bundledPath = path.join(path.dirname(getSchemasDir(apiName)), "bundled.json");
383
- const bundled = await fs.readJson(bundledPath);
384
- const sourceText = typeof bundled?.source === "string" ? bundled.source : typeof bundled?.sourceText === "string" ? bundled.sourceText : undefined;
385
- if (!sourceText) {
386
- throw new Error(`GraphQL source not found for API '${apiName}'. Run generate first.`);
387
- }
388
- return sourceText;
389
- }
390
- async function ensureGraphQLOperationSchema(apiName, operationId, force = false) {
391
- const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
392
- const schemaPath = getSchemaPath(apiName, sanitizedOperationId);
393
- if (!force && await fs.pathExists(schemaPath)) {
394
- return await fs.readJson(schemaPath);
395
- }
396
- const endpoints = await fs.readJson(getEndpointsPath(apiName));
397
- const endpoint = endpoints.find((entry) => entry.operationId === operationId || entry.name === operationId);
398
- if (!endpoint) {
399
- throw new Error(`Endpoint '${operationId}' not found in GraphQL endpoint list.`);
400
- }
401
- const sourceText = await readGraphQLBundledSource(apiName);
402
- const rootType = typeof endpoint.rootType === "string" ? endpoint.rootType : typeof endpoint.method === "string" ? endpoint.method : undefined;
403
- if (!rootType || (rootType !== "query" && rootType !== "mutation" && rootType !== "subscription")) {
404
- throw new Error(`Invalid GraphQL endpoint metadata for '${operationId}'.`);
405
- }
406
- const schema = await findGraphQLEndpoint(sourceText, rootType, operationId);
407
- await fs.ensureDir(path.dirname(schemaPath));
408
- await fs.writeJson(schemaPath, schema, { spaces: 2 });
409
- return schema;
410
- }
411
- async function getOperationSchemaType(apiName) {
412
- return await getSchemaType(apiName);
413
- }
414
- export function collectRequestUpdateTypeWarnings(requestJson, requestJsonUpdates) {
415
- const flattenedUpdates = flattenToDotNotation(requestJsonUpdates);
416
- const warnings = [];
417
- for (const [updatePath, updateValue] of Object.entries(flattenedUpdates)) {
418
- if (updateValue === DELETE_SENTINEL) {
419
- continue;
420
- }
421
- const expectedValue = getByPath(requestJson, updatePath);
422
- if (expectedValue === undefined) {
423
- continue;
424
- }
425
- if (!matchesExpectedTemplateValue(expectedValue, updateValue)) {
426
- warnings.push(`--update-request type mismatch at ${updatePath}: expected ${describeValueType(expectedValue)}, received ${describeValueType(updateValue)}.`);
427
- }
428
- }
429
- return warnings;
430
- }
431
- export async function ensureResponseSchema(apiName, operationId) {
432
- if (await getOperationSchemaType(apiName) === "graphql") {
433
- const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
434
- const responseSchemaPath = path.join(findRequestResponseDir(apiName, sanitizedOperationId), "response-schema.json");
435
- if (await fs.pathExists(responseSchemaPath)) {
436
- return await fs.readJson(responseSchemaPath);
437
- }
438
- const operationSchema = await ensureGraphQLOperationSchema(apiName, operationId, false);
439
- await fs.ensureDir(path.dirname(responseSchemaPath));
440
- await fs.writeJson(responseSchemaPath, operationSchema, { spaces: 2 });
441
- return operationSchema;
442
- }
443
- const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
444
- const schemasDir = getSchemasDir(apiName);
445
- const operationSchema = await loadJsonObject(path.resolve(schemasDir, `${sanitizedOperationId}.json`));
446
- const responseDir = findRequestResponseDir(apiName, sanitizedOperationId);
447
- const responseSchemaPath = path.join(responseDir, "response-schema.json");
448
- if (await fs.pathExists(responseSchemaPath)) {
449
- return await fs.readJson(responseSchemaPath);
450
- }
451
- const responseSchema = getDeterministicResponseBody(operationSchema);
452
- if (responseSchema !== undefined) {
453
- await fs.ensureDir(responseDir);
454
- await fs.writeJson(responseSchemaPath, responseSchema, { spaces: 2 });
455
- }
456
- return responseSchema;
457
- }
458
- export async function makeRequest(apiName, operationId, force = false, cliHeaders, requestJsonUpdates, requestJsonWarnings) {
459
- const clientSchema = await buildClientCodeSchema(apiName, operationId, await getSanitizedOperationId(apiName, operationId), force);
460
- const config = await loadConfig();
461
- const baseUrl = config.apis?.[apiName]?.baseUrl;
462
- if (!baseUrl)
463
- throw new Error("Base URL not found in config");
464
- if (clientSchema.schemaType === "graphql") {
465
- const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
466
- const gqlOperationSchema = await ensureGraphQLOperationSchema(apiName, operationId, force);
467
- const { requestJson: initialRequestJson, responseJsonPath } = await getOrCreateGraphQLRequestJson(apiName, sanitizedOperationId, force, gqlOperationSchema);
468
- const requestJsonPath = path.join(getSchemasDir(apiName), sanitizedOperationId, "request.json");
469
- let requestJson = initialRequestJson;
470
- const warnings = [];
471
- if (requestJsonWarnings && requestJsonWarnings.length > 0) {
472
- warnings.push(...requestJsonWarnings);
473
- }
474
- if (requestJsonUpdates && typeof requestJsonUpdates === "object" && Object.keys(requestJsonUpdates).length > 0) {
475
- const flattened = flattenToDotNotation(requestJsonUpdates);
476
- const mergedUpdates = { ...flattened };
477
- for (const [k, v] of Object.entries(requestJsonUpdates)) {
478
- if (!(k in mergedUpdates))
479
- mergedUpdates[k] = v;
480
- }
481
- await updateJsonFile(requestJsonPath, mergedUpdates, 2, { deleteSentinel: DELETE_SENTINEL });
482
- requestJson = await fs.readJson(requestJsonPath);
483
- }
484
- const requestVariables = {
485
- ...buildGraphQLVariables(clientSchema.args),
486
- ...(isGraphQLRequestJson(requestJson) ? requestJson.variables : {}),
487
- };
488
- const requestBody = {
489
- query: clientSchema.query,
490
- variables: requestVariables,
491
- };
492
- const requestContext = {
493
- headers: { "Content-Type": "application/json" },
494
- url: "",
495
- warnings,
496
- responseJsonPath,
497
- };
498
- const httpClient = new FetchClient(baseUrl);
499
- let liveResponse;
500
- try {
501
- liveResponse = await httpClient.post(baseUrl, {
502
- headers: requestContext.headers,
503
- body: requestBody,
504
- });
505
- if (liveResponse && typeof liveResponse === "object" && "status" in liveResponse) {
506
- const status = liveResponse.status;
507
- if (typeof status === "number" && (status < 200 || status >= 300)) {
508
- requestContext.warnings.push(`⚠️ The live HTTP request failed for ${baseUrl}: HTTP ${status}${"statusText" in liveResponse && typeof liveResponse.statusText === "string" && liveResponse.statusText ? ` ${liveResponse.statusText}` : ""}`);
509
- }
510
- }
511
- if (liveResponse && typeof liveResponse === "object" && Array.isArray(liveResponse.errors) && liveResponse.errors?.length) {
512
- requestContext.warnings.push("⚠️ GraphQL response contains errors.");
513
- }
514
- await fs.writeJson(requestContext.responseJsonPath, liveResponse, { spaces: 2 });
515
- }
516
- catch (err) {
517
- requestContext.warnings.push(`⚠️ The live HTTP request failed for ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`);
518
- if (!(await fs.pathExists(requestContext.responseJsonPath))) {
519
- requestContext.warnings.push(`response.json not found at ${requestContext.responseJsonPath}`);
520
- return { request: requestBody, response: undefined, warnings: requestContext.warnings };
521
- }
522
- }
523
- const responseJson = await fs.readJson(requestContext.responseJsonPath);
524
- return { request: requestBody, response: responseJson?.data?.[clientSchema.fieldName], warnings: requestContext.warnings };
525
- }
526
- const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
527
- const restSchema = await buildClientCodeSchema(apiName, operationId, sanitizedOperationId);
528
- if (restSchema.schemaType !== "rest") {
529
- throw new Error(`Invalid REST endpoint metadata for '${operationId}'.`);
530
- }
531
- const endpointSchema = await ensureEndpointSchemaFile(apiName, operationId, sanitizedOperationId, force);
532
- const { requestJson: initialRequestJson, responseJsonPath } = await getOrCreateRequestJson(apiName, sanitizedOperationId, force);
533
- const requestJsonPath = path.join(getSchemasDir(apiName), sanitizedOperationId, "request.json");
534
- let requestJson = initialRequestJson;
535
- const warnings = [];
536
- if (requestJsonWarnings && requestJsonWarnings.length > 0) {
537
- warnings.push(...requestJsonWarnings);
538
- }
539
- if (requestJsonUpdates && typeof requestJsonUpdates === "object" && Object.keys(requestJsonUpdates).length > 0) {
540
- const updateWarning = buildUpdateRequestWarning(requestJsonUpdates);
541
- if (updateWarning) {
542
- warnings.push(updateWarning);
543
- }
544
- const flattened = flattenToDotNotation(requestJsonUpdates);
545
- const mergedUpdates = { ...flattened };
546
- for (const [k, v] of Object.entries(requestJsonUpdates)) {
547
- if (!(k in mergedUpdates))
548
- mergedUpdates[k] = v;
549
- }
550
- await updateJsonFile(requestJsonPath, mergedUpdates, 2, { deleteSentinel: DELETE_SENTINEL });
551
- requestJson = await fs.readJson(requestJsonPath);
552
- }
553
- const requestContext = buildRequestContext(apiName, restSchema, requestJson, config, responseJsonPath, cliHeaders, warnings);
554
- const httpClient = new FetchClient(baseUrl);
555
- if (!restSchema.method || !restSchema.path) {
556
- throw new Error(`Invalid OpenAPI endpoint metadata for '${operationId}'.`);
557
- }
558
- const method = restSchema.method.toLowerCase();
559
- let liveResponse;
560
- try {
561
- switch (method) {
562
- case "get":
563
- liveResponse = await httpClient.get(requestContext.url, { headers: requestContext.headers });
564
- break;
565
- case "post":
566
- {
567
- const requestBodyTransport = await buildRequestBodyTransport(endpointSchema, requestJson);
568
- liveResponse = await httpClient.post(requestContext.url, {
569
- headers: {
570
- ...requestContext.headers,
571
- ...requestBodyTransport.headers,
572
- },
573
- body: requestBodyTransport.body,
574
- });
575
- }
576
- break;
577
- case "put":
578
- {
579
- const requestBodyTransport = await buildRequestBodyTransport(endpointSchema, requestJson);
580
- liveResponse = await httpClient.put(requestContext.url, {
581
- headers: {
582
- ...requestContext.headers,
583
- ...requestBodyTransport.headers,
584
- },
585
- body: requestBodyTransport.body,
586
- });
587
- }
588
- break;
589
- case "delete":
590
- liveResponse = await httpClient.delete(requestContext.url, { headers: requestContext.headers });
591
- break;
592
- case "patch":
593
- {
594
- const requestBodyTransport = await buildRequestBodyTransport(endpointSchema, requestJson);
595
- liveResponse = await httpClient.patch(requestContext.url, {
596
- headers: {
597
- ...requestContext.headers,
598
- ...requestBodyTransport.headers,
599
- },
600
- body: requestBodyTransport.body,
601
- });
602
- }
603
- break;
604
- case "head":
605
- liveResponse = await httpClient.head(requestContext.url, { headers: requestContext.headers });
606
- break;
607
- case "options":
608
- liveResponse = await httpClient.options(requestContext.url, { headers: requestContext.headers });
609
- break;
610
- default:
611
- throw new Error(`Unsupported HTTP method: ${restSchema.method}`);
612
- }
613
- if (liveResponse && typeof liveResponse === "object" && "status" in liveResponse) {
614
- const status = liveResponse.status;
615
- if (typeof status === "number" && (status < 200 || status >= 300)) {
616
- requestContext.warnings.push(`⚠️ The live HTTP request failed for ${baseUrl}${requestContext.url}: HTTP ${status}${"statusText" in liveResponse && typeof liveResponse.statusText === "string" && liveResponse.statusText ? ` ${liveResponse.statusText}` : ""}`);
617
- }
618
- }
619
- await fs.writeJson(requestContext.responseJsonPath, liveResponse, { spaces: 2 });
620
- }
621
- catch (err) {
622
- requestContext.warnings.push(`⚠️ The live HTTP request failed for ${baseUrl}${requestContext.url}: ${err instanceof Error ? err.message : String(err)}`);
623
- if (!(await fs.pathExists(requestContext.responseJsonPath))) {
624
- requestContext.warnings.push(`response.json not found at ${requestContext.responseJsonPath}`);
625
- return { request: requestJson, response: undefined, warnings: requestContext.warnings };
626
- }
627
- }
628
- const responseJson = await fs.readJson(requestContext.responseJsonPath);
629
- return { request: requestJson, response: responseJson, warnings: requestContext.warnings };
630
- }
631
- export async function validateResponse(apiName, operationId, force = false, cliHeaders, requestJsonUpdates, requestJsonWarnings) {
632
- const { request, response, warnings } = await makeRequest(apiName, operationId, force, cliHeaders, requestJsonUpdates, requestJsonWarnings);
633
- const safeWarnings = warnings ?? [];
634
- if (await getOperationSchemaType(apiName) === "graphql") {
635
- if (!response || typeof response !== "object") {
636
- return { valid: false, warnings: safeWarnings, errors: ["response.body missing or invalid"] };
637
- }
638
- const responseObject = response;
639
- if (Array.isArray(responseObject.errors) && responseObject.errors.length > 0) {
640
- return { valid: false, warnings: safeWarnings, errors: ["GraphQL response contains errors"] };
641
- }
642
- return { valid: true, warnings: safeWarnings };
643
- }
644
- const responseSchema = await ensureResponseSchema(apiName, operationId);
645
- if (responseSchema === undefined || Object.keys(responseSchema).length === 0) {
646
- warnings.push("No response schema found for this operation. Skipping validation.");
647
- return { valid: true, warnings: safeWarnings };
648
- }
649
- if (!response) {
650
- return { valid: false, warnings: safeWarnings };
651
- }
652
- const ajv = new Ajv({
653
- allErrors: true,
654
- strict: false,
655
- strictSchema: false,
656
- allowUnknownKeywords: true,
657
- removeAdditional: false
658
- });
659
- const validate = ajv.compile(responseSchema);
660
- const valid = validate(response);
661
- if (valid) {
662
- return { valid: true, warnings: safeWarnings };
663
- }
664
- else {
665
- const errors = (validate.errors || []).map((e) => {
666
- const path = e.instancePath ? e.instancePath.replace(/^\//, "response.body.").replace(/\//g, ".") : "response.body";
667
- return `${path} ${e.message}`;
668
- });
669
- return { valid: false, errors, warnings: safeWarnings };
670
- }
671
- }
672
- export async function prepareRequestTemplate(apiName, sanitizedOperationId, force = false) {
673
- if (await getOperationSchemaType(apiName) === "graphql") {
674
- const operationSchema = await ensureGraphQLOperationSchema(apiName, sanitizedOperationId, force);
675
- const requestJsonPath = path.join(getSchemasDir(apiName), sanitizedOperationId, "request.json");
676
- const responseJsonPath = path.join(findRequestResponseDir(apiName, sanitizedOperationId), "response.json");
677
- const requestJson = buildGraphQLArtifact(operationSchema);
678
- await fs.ensureDir(path.dirname(requestJsonPath));
679
- await fs.writeJson(requestJsonPath, requestJson, { spaces: 2 });
680
- return {
681
- requestJsonPath,
682
- responseJsonPath,
683
- requestJson,
684
- };
685
- }
686
- const { requestJson, responseJsonPath } = await getOrCreateRequestJson(apiName, sanitizedOperationId, force);
687
- const requestJsonPath = path.join(getSchemasDir(apiName), sanitizedOperationId, "request.json");
688
- return {
689
- requestJsonPath,
690
- responseJsonPath,
691
- requestJson,
692
- };
693
- }
694
- async function getOrCreateRequestJson(apiName, sanitizedOperationId, force) {
695
- const schemaPath = getSchemaPath(apiName, sanitizedOperationId);
696
- const fullSchema = await fs.readJson(schemaPath);
697
- const opDir = path.join(getSchemasDir(apiName), sanitizedOperationId);
698
- await fs.ensureDir(opDir);
699
- const requestJsonPath = path.join(opDir, "request.json");
700
- const responseJsonPath = path.join(opDir, "response.json");
701
- const shouldRegenerate = async () => {
702
- if (force || !(await fs.pathExists(requestJsonPath))) {
703
- return true;
704
- }
705
- const existingRequestJson = await fs.readJson(requestJsonPath);
706
- return !hasGeneratedRequestShape(existingRequestJson);
707
- };
708
- if (await shouldRegenerate()) {
709
- const template = buildDeterministicRequestTemplate(fullSchema);
710
- await fs.writeJson(requestJsonPath, template, { spaces: 2 });
711
- return {
712
- requestJson: template,
713
- responseJsonPath,
714
- };
715
- }
716
- return {
717
- requestJson: await fs.readJson(requestJsonPath),
718
- responseJsonPath,
719
- };
720
- }
721
- async function getOrCreateGraphQLRequestJson(apiName, sanitizedOperationId, force, operationSchema) {
722
- const opDir = path.join(getSchemasDir(apiName), sanitizedOperationId);
723
- await fs.ensureDir(opDir);
724
- const requestJsonPath = path.join(opDir, "request.json");
725
- const responseJsonPath = path.join(opDir, "response.json");
726
- const shouldRegenerate = async () => {
727
- if (force || !(await fs.pathExists(requestJsonPath))) {
728
- return true;
729
- }
730
- const existingRequestJson = await fs.readJson(requestJsonPath);
731
- return !isGraphQLRequestJson(existingRequestJson);
732
- };
733
- if (await shouldRegenerate()) {
734
- const template = buildGraphQLArtifact(operationSchema);
735
- await fs.writeJson(requestJsonPath, template, { spaces: 2 });
736
- return {
737
- requestJson: template,
738
- responseJsonPath,
739
- };
740
- }
741
- return {
742
- requestJson: await fs.readJson(requestJsonPath),
743
- responseJsonPath,
744
- };
745
- }
746
- function hasGeneratedRequestShape(value) {
747
- if (!value || typeof value !== "object" || Array.isArray(value)) {
748
- return false;
749
- }
750
- const requestJson = value;
751
- return Object.prototype.hasOwnProperty.call(requestJson, "parameters") && Array.isArray(requestJson.parameters);
752
- }
753
- function buildRequestContext(apiName, operationSchema, requestJson, config, responseJsonPath, cliHeaders, warnings = []) {
754
- const headers = {};
755
- const configHeaders = config.apis?.[apiName]?.auth?.headers;
756
- if (configHeaders && typeof configHeaders === "object") {
757
- Object.assign(headers, configHeaders);
758
- }
759
- if (cliHeaders && typeof cliHeaders === "object") {
760
- Object.assign(headers, cliHeaders);
761
- }
762
- const pathParams = {};
763
- const queryParams = {};
764
- for (const param of requestJson.parameters || []) {
765
- if (param.in === "path")
766
- pathParams[param.name] = param.value;
767
- else if (param.in === "query")
768
- queryParams[param.name] = param.value;
769
- else if (param.in === "header")
770
- headers[param.name] = String(param.value);
771
- }
772
- if (!operationSchema.path) {
773
- throw new Error(`Invalid OpenAPI endpoint metadata for '${operationSchema.operationId}'.`);
774
- }
775
- const urlPath = substitutePathParams(operationSchema.path, pathParams);
776
- const queryString = buildQueryString(queryParams);
777
- return {
778
- headers,
779
- url: urlPath + queryString,
780
- warnings,
781
- responseJsonPath,
782
- };
783
- }
784
- function buildQueryString(queryParams) {
785
- return Object.keys(queryParams).length
786
- ? "?" + Object.entries(queryParams).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&")
787
- : "";
788
- }
789
- function buildDeterministicRequestTemplate(fullSchema) {
790
- const parametersArr = [];
791
- if (Array.isArray(fullSchema.parameters)) {
792
- for (const param of fullSchema.parameters) {
793
- const value = getParameterDefaultValue(param);
794
- parametersArr.push({ name: param.name, in: param.in, value });
795
- }
796
- }
797
- let requestBodyObj = null;
798
- if (fullSchema?.requestBody || fullSchema?.parameters) {
799
- requestBodyObj = getDeterministicRequestBody(fullSchema);
800
- }
801
- const requestTemplate = {
802
- parameters: parametersArr,
803
- };
804
- if (requestBodyObj !== null) {
805
- requestTemplate.requestBody = requestBodyObj;
806
- }
807
- return requestTemplate;
808
- }
809
- function substitutePathParams(path, params) {
810
- return path.replace(/\{([^}]+)\}/g, (_, key) => {
811
- if (!(key in params))
812
- throw new Error(`Missing path parameter: ${key}`);
813
- return encodeURIComponent(params[key]);
814
- });
815
- }
816
- export function getDeterministicRequestBody(operation) {
817
- const oas3Schema = extractRequestSchemaOAS3(operation);
818
- if (oas3Schema) {
819
- return buildDeterministicTemplate(oas3Schema);
820
- }
821
- const oas2Schema = extractRequestSchemaOAS2(operation);
822
- if (oas2Schema) {
823
- return buildDeterministicTemplate(oas2Schema);
824
- }
825
- return null;
826
- }
827
- function extractRequestSchemaOAS3(operation) {
828
- const content = operation?.requestBody?.content;
829
- if (!content || typeof content !== "object")
830
- return null;
831
- for (const contentObj of Object.values(content)) {
832
- const typedContent = contentObj;
833
- if (typedContent?.schema) {
834
- return typedContent.schema;
835
- }
836
- }
837
- return null;
838
- }
839
- function extractRequestSchemaOAS2(operation) {
840
- const params = Array.isArray(operation?.parameters) ? operation.parameters : [];
841
- const bodyParam = params.find((p) => p.in === "body");
842
- if (bodyParam?.schema) {
843
- const schema = bodyParam.schema;
844
- if (!schema.type && schema.properties) {
845
- schema.type = "object";
846
- }
847
- return schema;
848
- }
849
- const consumes = operation?.consumes || [];
850
- const isForm = consumes.includes("application/x-www-form-urlencoded") ||
851
- consumes.includes("multipart/form-data");
852
- const formParams = params.filter((p) => p.in === "formData");
853
- if (isForm && formParams.length) {
854
- const schema = { type: "object", properties: {}, required: [] };
855
- for (const p of formParams) {
856
- const prop = {};
857
- if (p.schema)
858
- Object.assign(prop, p.schema);
859
- else {
860
- if (p.type)
861
- prop.type = p.type;
862
- if (p.items)
863
- prop.items = p.items;
864
- if (p.format)
865
- prop.format = p.format;
866
- if (p.enum)
867
- prop.enum = p.enum;
868
- if (p.type === "file") {
869
- prop.type = "string";
870
- prop.format = "binary";
871
- }
872
- }
873
- schema.properties[p.name] = prop;
874
- if (p.required)
875
- schema.required.push(p.name);
876
- }
877
- if (schema.required.length === 0)
878
- delete schema.required;
879
- return schema;
880
- }
881
- return null;
882
- }
883
- function buildDeterministicTemplate(schema) {
884
- if (!schema)
885
- return null;
886
- if (schema.const !== undefined) {
887
- return schema.const;
888
- }
889
- if (schema.default !== undefined) {
890
- return schema.default;
891
- }
892
- if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
893
- const merged = schema.allOf
894
- .map((item) => buildDeterministicTemplate(item))
895
- .filter((value) => value !== null && value !== undefined)
896
- .reduce((acc, value) => mergeDeterministicValues(acc, value), {});
897
- if (merged !== null && merged !== undefined) {
898
- return merged;
899
- }
900
- }
901
- if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
902
- return selectDeterministicVariant(schema.oneOf.map((item) => buildDeterministicTemplate(item)));
903
- }
904
- if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
905
- return selectDeterministicVariant(schema.anyOf.map((item) => buildDeterministicTemplate(item)));
906
- }
907
- if (!schema.type && schema.properties) {
908
- schema.type = "object";
909
- }
910
- if (schema.$ref) {
911
- throw new Error("Schema contains $ref. Resolve references before building template.");
912
- }
913
- switch (schema.type) {
914
- case "string":
915
- if (schema.format === "binary") {
916
- return {
917
- kind: "file",
918
- path: "",
919
- fileName: "",
920
- mimeType: "",
921
- };
922
- }
923
- return schema.example ?? "";
924
- case "integer":
925
- case "number":
926
- return schema.example ?? 0;
927
- case "boolean":
928
- return schema.example ?? false;
929
- case "array":
930
- return [];
931
- case "object":
932
- const obj = {};
933
- if (schema.properties) {
934
- for (const [key, propSchema] of Object.entries(schema.properties)) {
935
- obj[key] = buildDeterministicTemplate(propSchema);
936
- }
937
- }
938
- return obj;
939
- default:
940
- return null;
941
- }
942
- }
943
- function isPlainObject(value) {
944
- return !!value && typeof value === "object" && !Array.isArray(value);
945
- }
946
- function mergeDeterministicValues(left, right) {
947
- if (left === null || left === undefined) {
948
- return right;
949
- }
950
- if (right === null || right === undefined) {
951
- return left;
952
- }
953
- if (isPlainObject(left) && isPlainObject(right)) {
954
- const merged = { ...left };
955
- for (const [key, value] of Object.entries(right)) {
956
- merged[key] = key in merged ? mergeDeterministicValues(merged[key], value) : value;
957
- }
958
- return merged;
959
- }
960
- return right;
961
- }
962
- function selectDeterministicVariant(variants) {
963
- const candidates = variants.filter((value) => value !== null && value !== undefined);
964
- if (candidates.length === 0) {
965
- return null;
966
- }
967
- const objectCandidates = candidates.filter(isPlainObject);
968
- if (objectCandidates.length > 0) {
969
- return objectCandidates.reduce((best, candidate) => {
970
- const bestSize = Object.keys(best).length;
971
- const candidateSize = Object.keys(candidate).length;
972
- return candidateSize > bestSize ? candidate : best;
973
- });
974
- }
975
- return candidates[0];
976
- }
977
- function extractResponseSchemaOAS3(operation) {
978
- const responses = operation?.responses;
979
- if (!responses)
980
- return null;
981
- const status = responses["200"] ||
982
- responses["201"] ||
983
- responses["default"] ||
984
- Object.values(responses)[0];
985
- if (!status?.content)
986
- return null;
987
- const json = status.content["application/json"];
988
- if (json?.schema)
989
- return json.schema;
990
- for (const contentObj of Object.values(status.content)) {
991
- const typed = contentObj;
992
- if (typed.schema)
993
- return typed.schema;
994
- }
995
- return null;
996
- }
997
- function extractResponseSchemaOAS2(operation) {
998
- const responses = operation?.responses;
999
- if (!responses)
1000
- return {};
1001
- const status = responses["200"] ||
1002
- responses["201"] ||
1003
- responses["default"] ||
1004
- Object.values(responses)[0];
1005
- return status?.schema || {};
1006
- }
1007
- export function getDeterministicResponseBody(operation) {
1008
- const oas3Schema = extractResponseSchemaOAS3(operation);
1009
- if (oas3Schema) {
1010
- return oas3Schema;
1011
- }
1012
- const oas2Schema = extractResponseSchemaOAS2(operation);
1013
- if (oas2Schema) {
1014
- return oas2Schema;
1015
- }
1016
- }
1017
- //# sourceMappingURL=validate-response.js.map