nuxt-openapi-hyperfetch 0.1.7-alpha.1 → 0.2.7-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.
Files changed (97) hide show
  1. package/CONTRIBUTING.md +291 -292
  2. package/INSTRUCTIONS.md +327 -327
  3. package/LICENSE +202 -202
  4. package/README.md +231 -227
  5. package/dist/cli/logger.d.ts +26 -0
  6. package/dist/cli/logger.js +36 -0
  7. package/dist/cli/logo.js +5 -5
  8. package/dist/generators/components/connector-generator/generator.d.ts +12 -0
  9. package/dist/generators/components/connector-generator/generator.js +116 -0
  10. package/dist/generators/components/connector-generator/templates.d.ts +18 -0
  11. package/dist/generators/components/connector-generator/templates.js +222 -0
  12. package/dist/generators/components/connector-generator/types.d.ts +32 -0
  13. package/dist/generators/components/connector-generator/types.js +7 -0
  14. package/dist/generators/components/schema-analyzer/index.d.ts +17 -0
  15. package/dist/generators/components/schema-analyzer/index.js +20 -0
  16. package/dist/generators/components/schema-analyzer/intent-detector.d.ts +17 -0
  17. package/dist/generators/components/schema-analyzer/intent-detector.js +143 -0
  18. package/dist/generators/components/schema-analyzer/openapi-reader.d.ts +11 -0
  19. package/dist/generators/components/schema-analyzer/openapi-reader.js +76 -0
  20. package/dist/generators/components/schema-analyzer/resource-grouper.d.ts +6 -0
  21. package/dist/generators/components/schema-analyzer/resource-grouper.js +132 -0
  22. package/dist/generators/components/schema-analyzer/schema-field-mapper.d.ts +35 -0
  23. package/dist/generators/components/schema-analyzer/schema-field-mapper.js +220 -0
  24. package/dist/generators/components/schema-analyzer/types.d.ts +156 -0
  25. package/dist/generators/components/schema-analyzer/types.js +7 -0
  26. package/dist/generators/nuxt-server/generator.d.ts +2 -1
  27. package/dist/generators/nuxt-server/generator.js +21 -21
  28. package/dist/generators/shared/runtime/apiHelpers.d.ts +81 -41
  29. package/dist/generators/shared/runtime/apiHelpers.js +97 -104
  30. package/dist/generators/shared/runtime/pagination.d.ts +168 -0
  31. package/dist/generators/shared/runtime/pagination.js +179 -0
  32. package/dist/generators/shared/runtime/useDeleteConnector.d.ts +16 -0
  33. package/dist/generators/shared/runtime/useDeleteConnector.js +93 -0
  34. package/dist/generators/shared/runtime/useDetailConnector.d.ts +14 -0
  35. package/dist/generators/shared/runtime/useDetailConnector.js +50 -0
  36. package/dist/generators/shared/runtime/useFormConnector.d.ts +19 -0
  37. package/dist/generators/shared/runtime/useFormConnector.js +113 -0
  38. package/dist/generators/shared/runtime/useListConnector.d.ts +25 -0
  39. package/dist/generators/shared/runtime/useListConnector.js +125 -0
  40. package/dist/generators/shared/runtime/zod-error-merger.d.ts +23 -0
  41. package/dist/generators/shared/runtime/zod-error-merger.js +106 -0
  42. package/dist/generators/shared/templates/api-callbacks-plugin.js +54 -11
  43. package/dist/generators/shared/templates/api-pagination-plugin.d.ts +51 -0
  44. package/dist/generators/shared/templates/api-pagination-plugin.js +152 -0
  45. package/dist/generators/use-async-data/generator.d.ts +2 -1
  46. package/dist/generators/use-async-data/generator.js +14 -14
  47. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +114 -13
  48. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +88 -10
  49. package/dist/generators/use-async-data/templates.js +17 -17
  50. package/dist/generators/use-fetch/generator.d.ts +2 -1
  51. package/dist/generators/use-fetch/generator.js +12 -12
  52. package/dist/generators/use-fetch/runtime/useApiRequest.js +149 -40
  53. package/dist/generators/use-fetch/templates.js +14 -14
  54. package/dist/index.js +25 -0
  55. package/dist/module/index.d.ts +4 -0
  56. package/dist/module/index.js +93 -0
  57. package/dist/module/types.d.ts +27 -0
  58. package/dist/module/types.js +1 -0
  59. package/docs/API-REFERENCE.md +886 -887
  60. package/docs/generated-components.md +615 -0
  61. package/docs/headless-composables-ui.md +569 -0
  62. package/eslint.config.js +13 -0
  63. package/package.json +29 -2
  64. package/src/cli/config.ts +140 -140
  65. package/src/cli/logger.ts +124 -66
  66. package/src/cli/logo.ts +25 -25
  67. package/src/cli/types.ts +50 -50
  68. package/src/generators/components/connector-generator/generator.ts +138 -0
  69. package/src/generators/components/connector-generator/templates.ts +254 -0
  70. package/src/generators/components/connector-generator/types.ts +34 -0
  71. package/src/generators/components/schema-analyzer/index.ts +44 -0
  72. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -0
  73. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -0
  74. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -0
  75. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -0
  76. package/src/generators/components/schema-analyzer/types.ts +177 -0
  77. package/src/generators/nuxt-server/generator.ts +272 -270
  78. package/src/generators/shared/runtime/apiHelpers.ts +535 -507
  79. package/src/generators/shared/runtime/pagination.ts +323 -0
  80. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -0
  81. package/src/generators/shared/runtime/useDetailConnector.ts +64 -0
  82. package/src/generators/shared/runtime/useFormConnector.ts +139 -0
  83. package/src/generators/shared/runtime/useListConnector.ts +148 -0
  84. package/src/generators/shared/runtime/zod-error-merger.ts +119 -0
  85. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -352
  86. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -0
  87. package/src/generators/use-async-data/generator.ts +205 -204
  88. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -229
  89. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -245
  90. package/src/generators/use-async-data/templates.ts +257 -257
  91. package/src/generators/use-fetch/generator.ts +170 -169
  92. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -234
  93. package/src/generators/use-fetch/templates.ts +214 -214
  94. package/src/index.ts +303 -265
  95. package/src/module/index.ts +133 -0
  96. package/src/module/types.ts +31 -0
  97. package/src/generators/tanstack-query/generator.ts +0 -11
@@ -0,0 +1,138 @@
1
+ import * as path from 'path';
2
+ import fs from 'fs-extra';
3
+ import { fileURLToPath } from 'url';
4
+ import { format } from 'prettier';
5
+ import { analyzeSpec } from '../schema-analyzer/index.js';
6
+ import {
7
+ generateConnectorFile,
8
+ connectorFileName,
9
+ generateConnectorIndexFile,
10
+ } from './templates.js';
11
+ import type { ConnectorGeneratorOptions } from './types.js';
12
+ import { type Logger, createClackLogger } from '../../../cli/logger.js';
13
+
14
+ // Runtime files that must be copied to the user's project
15
+ const RUNTIME_FILES = [
16
+ 'useListConnector.ts',
17
+ 'useDetailConnector.ts',
18
+ 'useFormConnector.ts',
19
+ 'useDeleteConnector.ts',
20
+ 'zod-error-merger.ts',
21
+ ] as const;
22
+
23
+ /**
24
+ * Format TypeScript source with Prettier.
25
+ * Falls back to unformatted code on error.
26
+ */
27
+ async function formatCode(code: string, logger: Logger): Promise<string> {
28
+ try {
29
+ return await format(code, { parser: 'typescript' });
30
+ } catch (error) {
31
+ logger.log.warn(`Prettier formatting failed: ${String(error)}`);
32
+ return code;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Generate headless connector composables from an OpenAPI spec.
38
+ *
39
+ * Steps:
40
+ * 1. Analyze the spec → ResourceMap (Schema Analyzer)
41
+ * 2. For each resource: generate connector source, format, write
42
+ * 3. Write an index barrel file
43
+ * 4. Copy runtime helpers to the user's project
44
+ */
45
+ export async function generateConnectors(
46
+ options: ConnectorGeneratorOptions,
47
+ logger: Logger = createClackLogger()
48
+ ): Promise<void> {
49
+ const spinner = logger.spinner();
50
+
51
+ const outputDir = path.resolve(options.outputDir);
52
+ const composablesRelDir = options.composablesRelDir ?? '../use-async-data';
53
+ const runtimeRelDir = options.runtimeRelDir ?? '../runtime';
54
+
55
+ // ── 1. Analyze spec ───────────────────────────────────────────────────────
56
+ spinner.start('Analyzing OpenAPI spec');
57
+ const resourceMap = analyzeSpec(options.inputSpec);
58
+ spinner.stop(`Found ${resourceMap.size} resource(s)`);
59
+
60
+ if (resourceMap.size === 0) {
61
+ logger.log.warn('No resources found in spec — nothing to generate');
62
+ return;
63
+ }
64
+
65
+ // ── 2. Prepare output directory ───────────────────────────────────────────
66
+ // emptyDir (not ensureDir) so stale connectors from previous runs are removed.
67
+ // If the resource got renamed in the spec the old file would otherwise linger.
68
+ spinner.start('Preparing output directory');
69
+ await fs.emptyDir(outputDir);
70
+ spinner.stop('Output directory ready');
71
+
72
+ // ── 3. Generate connector files ───────────────────────────────────────────
73
+ spinner.start('Generating connector composables');
74
+ let successCount = 0;
75
+ let errorCount = 0;
76
+ const generatedNames: string[] = [];
77
+
78
+ for (const resource of resourceMap.values()) {
79
+ try {
80
+ const code = generateConnectorFile(resource, composablesRelDir);
81
+ const formatted = await formatCode(code, logger);
82
+ const fileName = connectorFileName(resource.composableName);
83
+ const filePath = path.join(outputDir, fileName);
84
+
85
+ await fs.writeFile(filePath, formatted, 'utf-8');
86
+ generatedNames.push(resource.composableName);
87
+ successCount++;
88
+ } catch (error) {
89
+ logger.log.error(`Error generating ${resource.composableName}: ${String(error)}`);
90
+ errorCount++;
91
+ }
92
+ }
93
+
94
+ spinner.stop(`Generated ${successCount} connector(s)`);
95
+
96
+ // ── 4. Write barrel index ─────────────────────────────────────────────────
97
+ if (generatedNames.length > 0) {
98
+ try {
99
+ const indexCode = generateConnectorIndexFile(generatedNames);
100
+ const formattedIndex = await formatCode(indexCode, logger);
101
+ await fs.writeFile(path.join(outputDir, 'index.ts'), formattedIndex, 'utf-8');
102
+ } catch (error) {
103
+ logger.log.warn(`Could not write connector index: ${String(error)}`);
104
+ }
105
+ }
106
+
107
+ // ── 5. Copy runtime helpers ───────────────────────────────────────────────
108
+ // Runtime files (useListConnector, useFormConnector …) live in src/ and must
109
+ // be physical .ts files in the user's project so Nuxt/Vite can type-check them.
110
+ //
111
+ // Path resolution trick:
112
+ // • During development (ts-node / tsx): __dirname ≈ src/generators/components/connector-generator/
113
+ // • After `tsc` build: __dirname ≈ dist/generators/components/connector-generator/
114
+ //
115
+ // In both cases we need to land in src/generators/shared/runtime/, so we step
116
+ // up 4 levels and then re-enter src/ explicitly. This works because the repo
117
+ // always keeps src/ alongside dist/ in the published package (see "files" in package.json).
118
+ spinner.start('Copying runtime files');
119
+ const runtimeDir = path.resolve(outputDir, runtimeRelDir);
120
+ await fs.ensureDir(runtimeDir); // ensureDir (not emptyDir) — other runtime files may exist there
121
+
122
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
123
+ const runtimeSrcDir = path.resolve(__dirname, '../../../../src/generators/shared/runtime');
124
+
125
+ for (const file of RUNTIME_FILES) {
126
+ const src = path.join(runtimeSrcDir, file);
127
+ const dest = path.join(runtimeDir, file);
128
+ await fs.copyFile(src, dest);
129
+ }
130
+
131
+ spinner.stop('Runtime files copied');
132
+
133
+ // ── 6. Summary ────────────────────────────────────────────────────────────
134
+ if (errorCount > 0) {
135
+ logger.log.warn(`Completed with ${errorCount} error(s)`);
136
+ }
137
+ logger.log.success(`Generated ${successCount} connector(s) in ${outputDir}`);
138
+ }
@@ -0,0 +1,254 @@
1
+ import { pascalCase, kebabCase } from 'change-case';
2
+ import type { ResourceInfo } from '../schema-analyzer/types.js';
3
+
4
+ // ─── File header ──────────────────────────────────────────────────────────────
5
+
6
+ function generateFileHeader(): string {
7
+ return `/**
8
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
9
+ *
10
+ * This file was automatically generated by nuxt-openapi-generator.
11
+ * Any manual changes will be overwritten on the next generation.
12
+ *
13
+ * @generated by nuxt-openapi-generator
14
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
15
+ */
16
+
17
+ /* eslint-disable */
18
+ // @ts-nocheck
19
+ `;
20
+ }
21
+
22
+ // ─── Naming helpers ───────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * operationId → useAsyncData composable name.
26
+ * 'getPets' → 'useAsyncDataGetPets'
27
+ */
28
+ function toAsyncDataName(operationId: string): string {
29
+ return `useAsyncData${pascalCase(operationId)}`;
30
+ }
31
+
32
+ /**
33
+ * composable name → kebab-case file name (without .ts).
34
+ * 'useAsyncDataGetPets' → 'use-async-data-get-pets'
35
+ */
36
+ function toFileName(composableName: string): string {
37
+ return kebabCase(composableName);
38
+ }
39
+
40
+ // ─── Section builders ─────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Build all `import` lines for a resource connector.
44
+ */
45
+ function buildImports(resource: ResourceInfo, composablesRelDir: string): string {
46
+ const lines: string[] = [];
47
+
48
+ // zod
49
+ lines.push(`import { z } from 'zod';`);
50
+ lines.push('');
51
+
52
+ // runtime helpers (Nuxt alias — set up by the Nuxt module)
53
+ const runtimeHelpers: string[] = [];
54
+ if (resource.listEndpoint) {
55
+ runtimeHelpers.push('useListConnector');
56
+ }
57
+ if (resource.detailEndpoint) {
58
+ runtimeHelpers.push('useDetailConnector');
59
+ }
60
+ if (resource.createEndpoint || resource.updateEndpoint) {
61
+ runtimeHelpers.push('useFormConnector');
62
+ }
63
+ if (resource.deleteEndpoint) {
64
+ runtimeHelpers.push('useDeleteConnector');
65
+ }
66
+
67
+ for (const helper of runtimeHelpers) {
68
+ lines.push(`import { ${helper} } from '#nxh/runtime/${helper}';`);
69
+ }
70
+ lines.push('');
71
+
72
+ // generated useAsyncData composables
73
+ const addImport = (operationId: string): void => {
74
+ const name = toAsyncDataName(operationId);
75
+ const file = toFileName(name);
76
+ lines.push(`import { ${name} } from '${composablesRelDir}/${file}';`);
77
+ };
78
+
79
+ if (resource.listEndpoint) {
80
+ addImport(resource.listEndpoint.operationId);
81
+ }
82
+ if (resource.detailEndpoint) {
83
+ addImport(resource.detailEndpoint.operationId);
84
+ }
85
+ if (resource.createEndpoint) {
86
+ addImport(resource.createEndpoint.operationId);
87
+ }
88
+ if (resource.updateEndpoint) {
89
+ addImport(resource.updateEndpoint.operationId);
90
+ }
91
+ if (resource.deleteEndpoint) {
92
+ addImport(resource.deleteEndpoint.operationId);
93
+ }
94
+
95
+ return lines.join('\n');
96
+ }
97
+
98
+ /**
99
+ * Build Zod schema const declarations.
100
+ */
101
+ function buildZodSchemas(resource: ResourceInfo): string {
102
+ const lines: string[] = [];
103
+ const pascal = pascalCase(resource.name);
104
+
105
+ if (resource.zodSchemas.create) {
106
+ lines.push(`const ${pascal}CreateSchema = ${resource.zodSchemas.create};`);
107
+ lines.push('');
108
+ }
109
+ if (resource.zodSchemas.update) {
110
+ lines.push(`const ${pascal}UpdateSchema = ${resource.zodSchemas.update};`);
111
+ lines.push('');
112
+ }
113
+
114
+ // Derive TS types via z.infer
115
+ if (resource.zodSchemas.create) {
116
+ lines.push(`type ${pascal}CreateInput = z.infer<typeof ${pascal}CreateSchema>;`);
117
+ }
118
+ if (resource.zodSchemas.update) {
119
+ lines.push(`type ${pascal}UpdateInput = z.infer<typeof ${pascal}UpdateSchema>;`);
120
+ }
121
+
122
+ return lines.join('\n');
123
+ }
124
+
125
+ /**
126
+ * Build the body of the exported connector function.
127
+ */
128
+ function buildFunctionBody(resource: ResourceInfo): string {
129
+ const pascal = pascalCase(resource.name);
130
+ const subConnectors: string[] = [];
131
+
132
+ if (resource.listEndpoint) {
133
+ const fn = toAsyncDataName(resource.listEndpoint.operationId);
134
+ // paginated: true tells useListConnector to expose pagination helpers
135
+ // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
136
+ // We set it whenever the spec declares a list endpoint that has a response schema,
137
+ // which is a reliable proxy for "this API returns structured data worth paginating".
138
+ const opts = resource.listEndpoint.responseSchema ? '{ paginated: true }' : '{}';
139
+ subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
140
+ }
141
+
142
+ if (resource.detailEndpoint) {
143
+ const fn = toAsyncDataName(resource.detailEndpoint.operationId);
144
+ subConnectors.push(` const detail = useDetailConnector(${fn});`);
145
+ }
146
+
147
+ if (resource.createEndpoint) {
148
+ const fn = toAsyncDataName(resource.createEndpoint.operationId);
149
+ const schemaArg = resource.zodSchemas.create ? `{ schema: ${pascal}CreateSchema }` : '{}';
150
+ subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
151
+ }
152
+
153
+ if (resource.updateEndpoint) {
154
+ const fn = toAsyncDataName(resource.updateEndpoint.operationId);
155
+ const hasDetail = !!resource.detailEndpoint;
156
+
157
+ // Build the options argument for useFormConnector:
158
+ // schema → Zod schema for client-side validation before submission
159
+ // loadWith → reference to the detail connector so the form auto-fills
160
+ // when detail.item changes (user clicks "Edit" on a row)
161
+ //
162
+ // Four combinations are possible depending on what the spec provides:
163
+ let schemaArg = '{}';
164
+ if (resource.zodSchemas.update && hasDetail) {
165
+ // Best case: validate AND pre-fill from detail
166
+ schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
167
+ } else if (resource.zodSchemas.update) {
168
+ // Validate, but no detail endpoint to pre-fill from
169
+ schemaArg = `{ schema: ${pascal}UpdateSchema }`;
170
+ } else if (hasDetail) {
171
+ // No Zod schema (no request body in spec), but still pre-fill from detail
172
+ schemaArg = `{ loadWith: detail }`;
173
+ }
174
+ subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg});`);
175
+ }
176
+
177
+ if (resource.deleteEndpoint) {
178
+ const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
179
+ subConnectors.push(` const deleteAction = useDeleteConnector(${fn});`);
180
+ }
181
+
182
+ // Return object — only include what was built
183
+ const returnKeys: string[] = [];
184
+ if (resource.listEndpoint) {
185
+ returnKeys.push('table');
186
+ }
187
+ if (resource.detailEndpoint) {
188
+ returnKeys.push('detail');
189
+ }
190
+ if (resource.createEndpoint) {
191
+ returnKeys.push('createForm');
192
+ }
193
+ if (resource.updateEndpoint) {
194
+ returnKeys.push('updateForm');
195
+ }
196
+ if (resource.deleteEndpoint) {
197
+ returnKeys.push('deleteAction');
198
+ }
199
+
200
+ const returnStatement = ` return { ${returnKeys.join(', ')} };`;
201
+
202
+ return [
203
+ `export function ${resource.composableName}() {`,
204
+ ...subConnectors,
205
+ returnStatement,
206
+ `}`,
207
+ ].join('\n');
208
+ }
209
+
210
+ // ─── Public API ───────────────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Generate the full source of a `use{Resource}Connector.ts` file.
214
+ *
215
+ * @param resource ResourceInfo produced by Schema Analyzer
216
+ * @param composablesRelDir Relative path from the connector dir to the
217
+ * useAsyncData composables dir (e.g. '../use-async-data')
218
+ */
219
+ export function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string): string {
220
+ const header = generateFileHeader();
221
+ const imports = buildImports(resource, composablesRelDir);
222
+ const schemas = buildZodSchemas(resource);
223
+ const fn = buildFunctionBody(resource);
224
+
225
+ // Assemble file: header + imports + (optional) Zod blocks + function body.
226
+ // Each section ends with its own trailing newline; join with \n adds one blank
227
+ // line between sections, which matches Prettier's output for this structure.
228
+ const parts: string[] = [header, imports];
229
+ if (schemas.trim()) {
230
+ parts.push(schemas);
231
+ }
232
+ parts.push(fn);
233
+
234
+ return parts.join('\n') + '\n';
235
+ }
236
+
237
+ /**
238
+ * Derive the output filename for a connector.
239
+ * 'usePetsConnector' → 'use-pets-connector.ts'
240
+ */
241
+ export function connectorFileName(composableName: string): string {
242
+ return `${kebabCase(composableName)}.ts`;
243
+ }
244
+
245
+ /**
246
+ * Generate an index barrel file that re-exports all connectors.
247
+ */
248
+ export function generateConnectorIndexFile(composableNames: string[]): string {
249
+ const header = generateFileHeader();
250
+ const exports = composableNames
251
+ .map((name) => `export { ${name} } from './${kebabCase(name)}';`)
252
+ .join('\n');
253
+ return `${header}${exports}\n`;
254
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Types for the Connector Generator — Fase 3.
3
+ *
4
+ * The Connector Generator reads the ResourceMap produced by the Schema Analyzer
5
+ * and writes one `use{Resource}Connector.ts` file per resource.
6
+ */
7
+
8
+ export interface ConnectorGeneratorOptions {
9
+ /** Absolute or relative path to the OpenAPI YAML/JSON spec */
10
+ inputSpec: string;
11
+ /** Directory where connector files will be written. E.g. ./composables/connectors */
12
+ outputDir: string;
13
+ /**
14
+ * Directory where the useAsyncData composables live, expressed as a path
15
+ * relative to outputDir. Defaults to '../use-async-data'.
16
+ */
17
+ composablesRelDir?: string;
18
+ /**
19
+ * Directory where runtime helpers will be copied to, expressed relative to
20
+ * outputDir. Defaults to '../runtime'.
21
+ */
22
+ runtimeRelDir?: string;
23
+ }
24
+
25
+ export interface ConnectorFileInfo {
26
+ /** PascalCase resource name. E.g. 'Pet' */
27
+ resourceName: string;
28
+ /** Generated composable function name. E.g. 'usePetsConnector' */
29
+ composableName: string;
30
+ /** Output filename (kebab-case). E.g. 'use-pets-connector.ts' */
31
+ fileName: string;
32
+ /** Formatted TypeScript source ready to be written to disk */
33
+ content: string;
34
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }