nuxt-openapi-hyperfetch 0.2.7-alpha.1 → 0.2.8-alpha.1
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/.editorconfig +26 -26
- package/.prettierignore +17 -17
- package/CONTRIBUTING.md +291 -291
- package/INSTRUCTIONS.md +327 -327
- package/LICENSE +202 -202
- package/README.md +231 -231
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +1 -1
- package/dist/cli/logo.js +5 -5
- package/dist/cli/messages.d.ts +1 -0
- package/dist/cli/messages.js +2 -0
- package/dist/cli/prompts.d.ts +5 -0
- package/dist/cli/prompts.js +12 -0
- package/dist/cli/types.d.ts +1 -1
- package/dist/generators/components/connector-generator/templates.js +12 -12
- package/dist/generators/use-async-data/templates.js +17 -17
- package/dist/generators/use-fetch/templates.js +14 -14
- package/dist/index.js +39 -27
- package/dist/module/index.js +19 -0
- package/dist/module/types.d.ts +7 -0
- package/docs/API-REFERENCE.md +886 -886
- package/docs/generated-components.md +615 -615
- package/docs/headless-composables-ui.md +569 -569
- package/eslint.config.js +85 -85
- package/package.json +1 -1
- package/src/cli/config.ts +147 -140
- package/src/cli/logger.ts +124 -124
- package/src/cli/logo.ts +25 -25
- package/src/cli/messages.ts +4 -0
- package/src/cli/prompts.ts +14 -1
- package/src/cli/types.ts +50 -50
- package/src/generators/components/connector-generator/generator.ts +138 -138
- package/src/generators/components/connector-generator/templates.ts +254 -254
- package/src/generators/components/connector-generator/types.ts +34 -34
- package/src/generators/components/schema-analyzer/index.ts +44 -44
- package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
- package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
- package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
- package/src/generators/components/schema-analyzer/types.ts +177 -177
- package/src/generators/nuxt-server/generator.ts +272 -272
- package/src/generators/shared/runtime/apiHelpers.ts +535 -535
- package/src/generators/shared/runtime/pagination.ts +323 -323
- package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
- package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
- package/src/generators/shared/runtime/useFormConnector.ts +139 -139
- package/src/generators/shared/runtime/useListConnector.ts +148 -148
- package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
- package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
- package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
- package/src/generators/use-async-data/generator.ts +205 -205
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
- package/src/generators/use-async-data/templates.ts +257 -257
- package/src/generators/use-fetch/generator.ts +170 -170
- package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
- package/src/generators/use-fetch/templates.ts +214 -214
- package/src/index.ts +305 -303
- package/src/module/index.ts +158 -133
- package/src/module/types.ts +39 -31
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
import type { EndpointInfo, Intent, OpenApiOperation, OpenApiPropertySchema } from './types.js';
|
|
2
|
-
|
|
3
|
-
// HTTP methods we care about
|
|
4
|
-
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH']);
|
|
5
|
-
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
|
6
|
-
type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
7
|
-
|
|
8
|
-
// ─── Path analysis helpers ────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
/** Returns path parameter names found in a path, e.g. '/pets/{id}' → ['id'] */
|
|
11
|
-
function extractPathParams(path: string): string[] {
|
|
12
|
-
const matches = path.match(/\{([^}]+)\}/g) ?? [];
|
|
13
|
-
return matches.map((m) => m.slice(1, -1));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** True when the path ends with a path parameter: /pets/{id} */
|
|
17
|
-
function endsWithPathParam(path: string): boolean {
|
|
18
|
-
return /\/\{[^}]+\}$/.test(path);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ─── Response schema analysis ─────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Return the resolved schema for the first 2xx response that has
|
|
25
|
-
* an application/json body, or undefined.
|
|
26
|
-
*/
|
|
27
|
-
function getSuccessResponseSchema(operation: OpenApiOperation): OpenApiPropertySchema | undefined {
|
|
28
|
-
if (!operation.responses) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
for (const [statusCode, response] of Object.entries(operation.responses)) {
|
|
33
|
-
const code = parseInt(statusCode, 10);
|
|
34
|
-
if (isNaN(code) || code < 200 || code >= 300) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const jsonContent = response.content?.['application/json'];
|
|
39
|
-
if (jsonContent?.schema) {
|
|
40
|
-
return jsonContent.schema;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** True when schema represents an array (type: array, or items present) */
|
|
48
|
-
function isArraySchema(schema: OpenApiPropertySchema): boolean {
|
|
49
|
-
return schema.type === 'array' || schema.items !== undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ─── Request body schema ──────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
function getRequestBodySchema(operation: OpenApiOperation): OpenApiPropertySchema | undefined {
|
|
55
|
-
if (!operation.requestBody?.content) {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const jsonContent = operation.requestBody.content['application/json'];
|
|
60
|
-
if (jsonContent?.schema) {
|
|
61
|
-
return jsonContent.schema;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Fallback to form-urlencoded
|
|
65
|
-
const formContent = operation.requestBody.content['application/x-www-form-urlencoded'];
|
|
66
|
-
return formContent?.schema;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ─── Intent detection ─────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Detect the CRUD intent of a single endpoint.
|
|
73
|
-
*
|
|
74
|
-
* Priority:
|
|
75
|
-
* 1. x-nxh-intent extension on the operation (developer override)
|
|
76
|
-
* 2. HTTP method + path pattern + response schema
|
|
77
|
-
*/
|
|
78
|
-
export function detectIntent(
|
|
79
|
-
method: HttpMethod,
|
|
80
|
-
path: string,
|
|
81
|
-
operation: OpenApiOperation
|
|
82
|
-
): Intent {
|
|
83
|
-
// 1. Developer override via OpenAPI extension
|
|
84
|
-
const override = operation['x-nxh-intent'];
|
|
85
|
-
if (override) {
|
|
86
|
-
return override;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const hasPathParam = extractPathParams(path).length > 0;
|
|
90
|
-
const responseSchema = getSuccessResponseSchema(operation);
|
|
91
|
-
|
|
92
|
-
switch (method) {
|
|
93
|
-
case 'DELETE':
|
|
94
|
-
return 'delete';
|
|
95
|
-
|
|
96
|
-
case 'POST':
|
|
97
|
-
// POST /resource → create
|
|
98
|
-
// POST /resource/{id}/action → unknown (custom action, not CRUD)
|
|
99
|
-
return !endsWithPathParam(path) ? 'create' : 'unknown';
|
|
100
|
-
|
|
101
|
-
case 'PUT':
|
|
102
|
-
case 'PATCH':
|
|
103
|
-
return 'update';
|
|
104
|
-
|
|
105
|
-
case 'GET': {
|
|
106
|
-
// A GET without a JSON response (e.g. binary download) is not a CRUD intent
|
|
107
|
-
if (!responseSchema) {
|
|
108
|
-
return 'unknown';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Array response ( type: 'array' OR has 'items' ) → always a list
|
|
112
|
-
if (isArraySchema(responseSchema)) {
|
|
113
|
-
return 'list';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Object response — distinguish list vs detail by path structure:
|
|
117
|
-
// GET /pets/{id} → has path param → detail (single item fetch)
|
|
118
|
-
// GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
|
|
119
|
-
if (hasPathParam) {
|
|
120
|
-
return 'detail';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return 'list';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
default:
|
|
127
|
-
return 'unknown';
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ─── Endpoint extraction ──────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Extract all endpoints from a single path item as EndpointInfo[].
|
|
135
|
-
* The spec must already be $ref-resolved before calling this.
|
|
136
|
-
*/
|
|
137
|
-
export function extractEndpoints(
|
|
138
|
-
path: string,
|
|
139
|
-
pathItem: Record<string, OpenApiOperation>
|
|
140
|
-
): EndpointInfo[] {
|
|
141
|
-
const results: EndpointInfo[] = [];
|
|
142
|
-
const pathParams = extractPathParams(path);
|
|
143
|
-
|
|
144
|
-
for (const method of HTTP_METHODS) {
|
|
145
|
-
const operation: OpenApiOperation | undefined = pathItem[method.toLowerCase()];
|
|
146
|
-
|
|
147
|
-
if (!operation) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const intent = detectIntent(method, path, operation);
|
|
152
|
-
|
|
153
|
-
const endpoint: EndpointInfo = {
|
|
154
|
-
// Fallback operationId when the spec omits it: 'get_/pets/{id}' → 'get__pets__id_'
|
|
155
|
-
// This rarely produces a ideal composable name, but avoids a crash.
|
|
156
|
-
operationId: operation.operationId ?? `${method.toLowerCase()}_${path.replace(/\//g, '_')}`,
|
|
157
|
-
method,
|
|
158
|
-
path,
|
|
159
|
-
tags: operation.tags ?? [],
|
|
160
|
-
summary: operation.summary,
|
|
161
|
-
description: operation.description,
|
|
162
|
-
intent,
|
|
163
|
-
hasPathParams: pathParams.length > 0,
|
|
164
|
-
pathParams,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Attach response schema for GET intents
|
|
168
|
-
if (method === 'GET') {
|
|
169
|
-
const schema = getSuccessResponseSchema(operation);
|
|
170
|
-
if (schema) {
|
|
171
|
-
endpoint.responseSchema = schema as import('./types.js').OpenApiSchema;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Attach request body schema for mutating methods
|
|
176
|
-
if (MUTATING_METHODS.has(method)) {
|
|
177
|
-
const schema = getRequestBodySchema(operation);
|
|
178
|
-
if (schema) {
|
|
179
|
-
endpoint.requestBodySchema = schema as import('./types.js').OpenApiSchema;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
results.push(endpoint);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return results;
|
|
187
|
-
}
|
|
1
|
+
import type { EndpointInfo, Intent, OpenApiOperation, OpenApiPropertySchema } from './types.js';
|
|
2
|
+
|
|
3
|
+
// HTTP methods we care about
|
|
4
|
+
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH']);
|
|
5
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
|
6
|
+
type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
7
|
+
|
|
8
|
+
// ─── Path analysis helpers ────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/** Returns path parameter names found in a path, e.g. '/pets/{id}' → ['id'] */
|
|
11
|
+
function extractPathParams(path: string): string[] {
|
|
12
|
+
const matches = path.match(/\{([^}]+)\}/g) ?? [];
|
|
13
|
+
return matches.map((m) => m.slice(1, -1));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when the path ends with a path parameter: /pets/{id} */
|
|
17
|
+
function endsWithPathParam(path: string): boolean {
|
|
18
|
+
return /\/\{[^}]+\}$/.test(path);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Response schema analysis ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return the resolved schema for the first 2xx response that has
|
|
25
|
+
* an application/json body, or undefined.
|
|
26
|
+
*/
|
|
27
|
+
function getSuccessResponseSchema(operation: OpenApiOperation): OpenApiPropertySchema | undefined {
|
|
28
|
+
if (!operation.responses) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [statusCode, response] of Object.entries(operation.responses)) {
|
|
33
|
+
const code = parseInt(statusCode, 10);
|
|
34
|
+
if (isNaN(code) || code < 200 || code >= 300) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const jsonContent = response.content?.['application/json'];
|
|
39
|
+
if (jsonContent?.schema) {
|
|
40
|
+
return jsonContent.schema;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when schema represents an array (type: array, or items present) */
|
|
48
|
+
function isArraySchema(schema: OpenApiPropertySchema): boolean {
|
|
49
|
+
return schema.type === 'array' || schema.items !== undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Request body schema ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function getRequestBodySchema(operation: OpenApiOperation): OpenApiPropertySchema | undefined {
|
|
55
|
+
if (!operation.requestBody?.content) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const jsonContent = operation.requestBody.content['application/json'];
|
|
60
|
+
if (jsonContent?.schema) {
|
|
61
|
+
return jsonContent.schema;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback to form-urlencoded
|
|
65
|
+
const formContent = operation.requestBody.content['application/x-www-form-urlencoded'];
|
|
66
|
+
return formContent?.schema;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Intent detection ─────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Detect the CRUD intent of a single endpoint.
|
|
73
|
+
*
|
|
74
|
+
* Priority:
|
|
75
|
+
* 1. x-nxh-intent extension on the operation (developer override)
|
|
76
|
+
* 2. HTTP method + path pattern + response schema
|
|
77
|
+
*/
|
|
78
|
+
export function detectIntent(
|
|
79
|
+
method: HttpMethod,
|
|
80
|
+
path: string,
|
|
81
|
+
operation: OpenApiOperation
|
|
82
|
+
): Intent {
|
|
83
|
+
// 1. Developer override via OpenAPI extension
|
|
84
|
+
const override = operation['x-nxh-intent'];
|
|
85
|
+
if (override) {
|
|
86
|
+
return override;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const hasPathParam = extractPathParams(path).length > 0;
|
|
90
|
+
const responseSchema = getSuccessResponseSchema(operation);
|
|
91
|
+
|
|
92
|
+
switch (method) {
|
|
93
|
+
case 'DELETE':
|
|
94
|
+
return 'delete';
|
|
95
|
+
|
|
96
|
+
case 'POST':
|
|
97
|
+
// POST /resource → create
|
|
98
|
+
// POST /resource/{id}/action → unknown (custom action, not CRUD)
|
|
99
|
+
return !endsWithPathParam(path) ? 'create' : 'unknown';
|
|
100
|
+
|
|
101
|
+
case 'PUT':
|
|
102
|
+
case 'PATCH':
|
|
103
|
+
return 'update';
|
|
104
|
+
|
|
105
|
+
case 'GET': {
|
|
106
|
+
// A GET without a JSON response (e.g. binary download) is not a CRUD intent
|
|
107
|
+
if (!responseSchema) {
|
|
108
|
+
return 'unknown';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Array response ( type: 'array' OR has 'items' ) → always a list
|
|
112
|
+
if (isArraySchema(responseSchema)) {
|
|
113
|
+
return 'list';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Object response — distinguish list vs detail by path structure:
|
|
117
|
+
// GET /pets/{id} → has path param → detail (single item fetch)
|
|
118
|
+
// GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
|
|
119
|
+
if (hasPathParam) {
|
|
120
|
+
return 'detail';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return 'list';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
return 'unknown';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Endpoint extraction ──────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract all endpoints from a single path item as EndpointInfo[].
|
|
135
|
+
* The spec must already be $ref-resolved before calling this.
|
|
136
|
+
*/
|
|
137
|
+
export function extractEndpoints(
|
|
138
|
+
path: string,
|
|
139
|
+
pathItem: Record<string, OpenApiOperation>
|
|
140
|
+
): EndpointInfo[] {
|
|
141
|
+
const results: EndpointInfo[] = [];
|
|
142
|
+
const pathParams = extractPathParams(path);
|
|
143
|
+
|
|
144
|
+
for (const method of HTTP_METHODS) {
|
|
145
|
+
const operation: OpenApiOperation | undefined = pathItem[method.toLowerCase()];
|
|
146
|
+
|
|
147
|
+
if (!operation) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const intent = detectIntent(method, path, operation);
|
|
152
|
+
|
|
153
|
+
const endpoint: EndpointInfo = {
|
|
154
|
+
// Fallback operationId when the spec omits it: 'get_/pets/{id}' → 'get__pets__id_'
|
|
155
|
+
// This rarely produces a ideal composable name, but avoids a crash.
|
|
156
|
+
operationId: operation.operationId ?? `${method.toLowerCase()}_${path.replace(/\//g, '_')}`,
|
|
157
|
+
method,
|
|
158
|
+
path,
|
|
159
|
+
tags: operation.tags ?? [],
|
|
160
|
+
summary: operation.summary,
|
|
161
|
+
description: operation.description,
|
|
162
|
+
intent,
|
|
163
|
+
hasPathParams: pathParams.length > 0,
|
|
164
|
+
pathParams,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Attach response schema for GET intents
|
|
168
|
+
if (method === 'GET') {
|
|
169
|
+
const schema = getSuccessResponseSchema(operation);
|
|
170
|
+
if (schema) {
|
|
171
|
+
endpoint.responseSchema = schema as import('./types.js').OpenApiSchema;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Attach request body schema for mutating methods
|
|
176
|
+
if (MUTATING_METHODS.has(method)) {
|
|
177
|
+
const schema = getRequestBodySchema(operation);
|
|
178
|
+
if (schema) {
|
|
179
|
+
endpoint.requestBodySchema = schema as import('./types.js').OpenApiSchema;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
results.push(endpoint);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { load as loadYaml } from 'js-yaml';
|
|
4
|
-
import type { OpenApiSpec, OpenApiPropertySchema } from './types.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Read an OpenAPI spec from a YAML or JSON file.
|
|
8
|
-
* Returns the parsed spec with all $ref values resolved inline.
|
|
9
|
-
*/
|
|
10
|
-
export function readOpenApiSpec(filePath: string): OpenApiSpec {
|
|
11
|
-
const absPath = path.resolve(filePath);
|
|
12
|
-
const content = readFileSync(absPath, 'utf-8');
|
|
13
|
-
|
|
14
|
-
const raw = filePath.endsWith('.json')
|
|
15
|
-
? (JSON.parse(content) as OpenApiSpec)
|
|
16
|
-
: (loadYaml(content) as OpenApiSpec);
|
|
17
|
-
|
|
18
|
-
if (!raw || typeof raw !== 'object' || !raw.openapi || !raw.paths) {
|
|
19
|
-
throw new Error(`Invalid OpenAPI spec: ${absPath}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return resolveRefs(raw, raw) as OpenApiSpec;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── $ref resolver ────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Recursively walk the document and replace every { $ref: '#/...' } with the
|
|
29
|
-
* referenced value (deep-cloned to avoid circular references).
|
|
30
|
-
* Only local JSON Pointer refs (#/...) are supported.
|
|
31
|
-
*/
|
|
32
|
-
function resolveRefs(node: unknown, root: OpenApiSpec, visited = new Set<string>()): unknown {
|
|
33
|
-
if (node === null || typeof node !== 'object') {
|
|
34
|
-
return node;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (Array.isArray(node)) {
|
|
38
|
-
return node.map((item) => resolveRefs(item, root, visited));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const obj = node as Record<string, unknown>;
|
|
42
|
-
|
|
43
|
-
if (typeof obj['$ref'] === 'string') {
|
|
44
|
-
const ref = obj['$ref'];
|
|
45
|
-
|
|
46
|
-
if (visited.has(ref)) {
|
|
47
|
-
// Circular ref protection — return empty object rather than infinite loop
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const resolved = resolvePointer(root, ref);
|
|
52
|
-
const newVisited = new Set(visited);
|
|
53
|
-
newVisited.add(ref);
|
|
54
|
-
return resolveRefs(resolved, root, newVisited);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const result: Record<string, unknown> = {};
|
|
58
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
59
|
-
result[key] = resolveRefs(value, root, visited);
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Resolve a JSON Pointer like '#/components/schemas/Pet' against the root doc.
|
|
66
|
-
*/
|
|
67
|
-
function resolvePointer(root: unknown, ref: string): unknown {
|
|
68
|
-
if (!ref.startsWith('#/')) {
|
|
69
|
-
throw new Error(`Only local $ref values are supported (got: ${ref})`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const parts = ref
|
|
73
|
-
.slice(2)
|
|
74
|
-
.split('/')
|
|
75
|
-
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
76
|
-
|
|
77
|
-
let current: Record<string, unknown> = root as Record<string, unknown>;
|
|
78
|
-
for (const part of parts) {
|
|
79
|
-
if (current === null || typeof current !== 'object' || !(part in current)) {
|
|
80
|
-
throw new Error(`Cannot resolve $ref: ${ref}`);
|
|
81
|
-
}
|
|
82
|
-
current = current[part] as Record<string, unknown>;
|
|
83
|
-
}
|
|
84
|
-
return current;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Resolve a single inline schema that may still have $ref (convenience helper
|
|
89
|
-
* used by other modules that receive already-partially-resolved specs).
|
|
90
|
-
*/
|
|
91
|
-
export function resolveSchema(
|
|
92
|
-
schema: OpenApiPropertySchema,
|
|
93
|
-
root: OpenApiSpec
|
|
94
|
-
): OpenApiPropertySchema {
|
|
95
|
-
return resolveRefs(schema, root) as OpenApiPropertySchema;
|
|
96
|
-
}
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { load as loadYaml } from 'js-yaml';
|
|
4
|
+
import type { OpenApiSpec, OpenApiPropertySchema } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read an OpenAPI spec from a YAML or JSON file.
|
|
8
|
+
* Returns the parsed spec with all $ref values resolved inline.
|
|
9
|
+
*/
|
|
10
|
+
export function readOpenApiSpec(filePath: string): OpenApiSpec {
|
|
11
|
+
const absPath = path.resolve(filePath);
|
|
12
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
13
|
+
|
|
14
|
+
const raw = filePath.endsWith('.json')
|
|
15
|
+
? (JSON.parse(content) as OpenApiSpec)
|
|
16
|
+
: (loadYaml(content) as OpenApiSpec);
|
|
17
|
+
|
|
18
|
+
if (!raw || typeof raw !== 'object' || !raw.openapi || !raw.paths) {
|
|
19
|
+
throw new Error(`Invalid OpenAPI spec: ${absPath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return resolveRefs(raw, raw) as OpenApiSpec;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── $ref resolver ────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively walk the document and replace every { $ref: '#/...' } with the
|
|
29
|
+
* referenced value (deep-cloned to avoid circular references).
|
|
30
|
+
* Only local JSON Pointer refs (#/...) are supported.
|
|
31
|
+
*/
|
|
32
|
+
function resolveRefs(node: unknown, root: OpenApiSpec, visited = new Set<string>()): unknown {
|
|
33
|
+
if (node === null || typeof node !== 'object') {
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (Array.isArray(node)) {
|
|
38
|
+
return node.map((item) => resolveRefs(item, root, visited));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const obj = node as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
if (typeof obj['$ref'] === 'string') {
|
|
44
|
+
const ref = obj['$ref'];
|
|
45
|
+
|
|
46
|
+
if (visited.has(ref)) {
|
|
47
|
+
// Circular ref protection — return empty object rather than infinite loop
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolved = resolvePointer(root, ref);
|
|
52
|
+
const newVisited = new Set(visited);
|
|
53
|
+
newVisited.add(ref);
|
|
54
|
+
return resolveRefs(resolved, root, newVisited);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result: Record<string, unknown> = {};
|
|
58
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
59
|
+
result[key] = resolveRefs(value, root, visited);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a JSON Pointer like '#/components/schemas/Pet' against the root doc.
|
|
66
|
+
*/
|
|
67
|
+
function resolvePointer(root: unknown, ref: string): unknown {
|
|
68
|
+
if (!ref.startsWith('#/')) {
|
|
69
|
+
throw new Error(`Only local $ref values are supported (got: ${ref})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parts = ref
|
|
73
|
+
.slice(2)
|
|
74
|
+
.split('/')
|
|
75
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
76
|
+
|
|
77
|
+
let current: Record<string, unknown> = root as Record<string, unknown>;
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
if (current === null || typeof current !== 'object' || !(part in current)) {
|
|
80
|
+
throw new Error(`Cannot resolve $ref: ${ref}`);
|
|
81
|
+
}
|
|
82
|
+
current = current[part] as Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a single inline schema that may still have $ref (convenience helper
|
|
89
|
+
* used by other modules that receive already-partially-resolved specs).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveSchema(
|
|
92
|
+
schema: OpenApiPropertySchema,
|
|
93
|
+
root: OpenApiSpec
|
|
94
|
+
): OpenApiPropertySchema {
|
|
95
|
+
return resolveRefs(schema, root) as OpenApiPropertySchema;
|
|
96
|
+
}
|