nuxt-openapi-hyperfetch 0.2.7-alpha.1 → 0.3.0-beta

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 (68) hide show
  1. package/.editorconfig +26 -26
  2. package/.prettierignore +17 -17
  3. package/CONTRIBUTING.md +291 -291
  4. package/INSTRUCTIONS.md +327 -327
  5. package/LICENSE +202 -202
  6. package/README.md +309 -231
  7. package/dist/cli/config.d.ts +9 -2
  8. package/dist/cli/config.js +1 -1
  9. package/dist/cli/logo.js +5 -5
  10. package/dist/cli/messages.d.ts +1 -0
  11. package/dist/cli/messages.js +2 -0
  12. package/dist/cli/prompts.d.ts +5 -0
  13. package/dist/cli/prompts.js +12 -0
  14. package/dist/cli/types.d.ts +1 -1
  15. package/dist/generators/components/connector-generator/templates.js +68 -19
  16. package/dist/generators/shared/runtime/useFormConnector.js +8 -1
  17. package/dist/generators/shared/runtime/useListConnector.js +13 -6
  18. package/dist/generators/use-async-data/generator.js +4 -0
  19. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
  20. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
  21. package/dist/generators/use-async-data/templates.js +17 -17
  22. package/dist/generators/use-fetch/generator.js +4 -0
  23. package/dist/generators/use-fetch/templates.js +14 -14
  24. package/dist/index.js +40 -27
  25. package/dist/module/index.js +19 -0
  26. package/dist/module/types.d.ts +7 -0
  27. package/docs/API-REFERENCE.md +886 -886
  28. package/docs/generated-components.md +615 -615
  29. package/docs/headless-composables-ui.md +569 -569
  30. package/eslint.config.js +85 -85
  31. package/package.json +1 -1
  32. package/src/cli/config.ts +147 -140
  33. package/src/cli/logger.ts +124 -124
  34. package/src/cli/logo.ts +25 -25
  35. package/src/cli/messages.ts +4 -0
  36. package/src/cli/prompts.ts +14 -1
  37. package/src/cli/types.ts +50 -50
  38. package/src/generators/components/connector-generator/generator.ts +138 -138
  39. package/src/generators/components/connector-generator/templates.ts +307 -254
  40. package/src/generators/components/connector-generator/types.ts +34 -34
  41. package/src/generators/components/schema-analyzer/index.ts +44 -44
  42. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
  43. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
  44. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
  45. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
  46. package/src/generators/components/schema-analyzer/types.ts +177 -177
  47. package/src/generators/nuxt-server/generator.ts +272 -272
  48. package/src/generators/shared/runtime/apiHelpers.ts +535 -535
  49. package/src/generators/shared/runtime/pagination.ts +323 -323
  50. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
  51. package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
  52. package/src/generators/shared/runtime/useFormConnector.ts +147 -139
  53. package/src/generators/shared/runtime/useListConnector.ts +158 -148
  54. package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
  55. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
  56. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
  57. package/src/generators/use-async-data/generator.ts +213 -205
  58. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
  59. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
  60. package/src/generators/use-async-data/templates.ts +257 -257
  61. package/src/generators/use-fetch/generator.ts +178 -170
  62. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
  63. package/src/generators/use-fetch/templates.ts +214 -214
  64. package/src/index.ts +306 -303
  65. package/src/module/index.ts +158 -133
  66. package/src/module/types.ts +39 -31
  67. package/dist/generators/tanstack-query/generator.d.ts +0 -5
  68. package/dist/generators/tanstack-query/generator.js +0 -11
@@ -1,44 +1,44 @@
1
- /**
2
- * Schema Analyzer — entry point
3
- *
4
- * Usage:
5
- * import { analyzeSpec } from './schema-analyzer/index.js'
6
- * const resourceMap = analyzeSpec('./swagger.yaml')
7
- */
8
-
9
- export { readOpenApiSpec } from './openapi-reader.js';
10
- export { detectIntent, extractEndpoints } from './intent-detector.js';
11
- export { buildResourceMap } from './resource-grouper.js';
12
- export {
13
- mapFieldsFromSchema,
14
- mapColumnsFromSchema,
15
- buildZodSchema,
16
- zodExpressionFromProp,
17
- } from './schema-field-mapper.js';
18
- export type {
19
- OpenApiSpec,
20
- OpenApiSchema,
21
- OpenApiPropertySchema,
22
- OpenApiOperation,
23
- OpenApiParameter,
24
- EndpointInfo,
25
- ResourceInfo,
26
- ResourceMap,
27
- FormFieldDef,
28
- ColumnDef,
29
- Intent,
30
- FieldType,
31
- ColumnType,
32
- } from './types.js';
33
-
34
- import { readOpenApiSpec } from './openapi-reader.js';
35
- import { buildResourceMap } from './resource-grouper.js';
36
- import type { ResourceMap } from './types.js';
37
-
38
- /**
39
- * Convenience function: read a spec file and return the full ResourceMap.
40
- */
41
- export function analyzeSpec(specPath: string): ResourceMap {
42
- const spec = readOpenApiSpec(specPath);
43
- return buildResourceMap(spec);
44
- }
1
+ /**
2
+ * Schema Analyzer — entry point
3
+ *
4
+ * Usage:
5
+ * import { analyzeSpec } from './schema-analyzer/index.js'
6
+ * const resourceMap = analyzeSpec('./swagger.yaml')
7
+ */
8
+
9
+ export { readOpenApiSpec } from './openapi-reader.js';
10
+ export { detectIntent, extractEndpoints } from './intent-detector.js';
11
+ export { buildResourceMap } from './resource-grouper.js';
12
+ export {
13
+ mapFieldsFromSchema,
14
+ mapColumnsFromSchema,
15
+ buildZodSchema,
16
+ zodExpressionFromProp,
17
+ } from './schema-field-mapper.js';
18
+ export type {
19
+ OpenApiSpec,
20
+ OpenApiSchema,
21
+ OpenApiPropertySchema,
22
+ OpenApiOperation,
23
+ OpenApiParameter,
24
+ EndpointInfo,
25
+ ResourceInfo,
26
+ ResourceMap,
27
+ FormFieldDef,
28
+ ColumnDef,
29
+ Intent,
30
+ FieldType,
31
+ ColumnType,
32
+ } from './types.js';
33
+
34
+ import { readOpenApiSpec } from './openapi-reader.js';
35
+ import { buildResourceMap } from './resource-grouper.js';
36
+ import type { ResourceMap } from './types.js';
37
+
38
+ /**
39
+ * Convenience function: read a spec file and return the full ResourceMap.
40
+ */
41
+ export function analyzeSpec(specPath: string): ResourceMap {
42
+ const spec = readOpenApiSpec(specPath);
43
+ return buildResourceMap(spec);
44
+ }
@@ -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
+ }