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,116 @@
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 { generateConnectorFile, connectorFileName, generateConnectorIndexFile, } from './templates.js';
7
+ import { createClackLogger } from '../../../cli/logger.js';
8
+ // Runtime files that must be copied to the user's project
9
+ const RUNTIME_FILES = [
10
+ 'useListConnector.ts',
11
+ 'useDetailConnector.ts',
12
+ 'useFormConnector.ts',
13
+ 'useDeleteConnector.ts',
14
+ 'zod-error-merger.ts',
15
+ ];
16
+ /**
17
+ * Format TypeScript source with Prettier.
18
+ * Falls back to unformatted code on error.
19
+ */
20
+ async function formatCode(code, logger) {
21
+ try {
22
+ return await format(code, { parser: 'typescript' });
23
+ }
24
+ catch (error) {
25
+ logger.log.warn(`Prettier formatting failed: ${String(error)}`);
26
+ return code;
27
+ }
28
+ }
29
+ /**
30
+ * Generate headless connector composables from an OpenAPI spec.
31
+ *
32
+ * Steps:
33
+ * 1. Analyze the spec → ResourceMap (Schema Analyzer)
34
+ * 2. For each resource: generate connector source, format, write
35
+ * 3. Write an index barrel file
36
+ * 4. Copy runtime helpers to the user's project
37
+ */
38
+ export async function generateConnectors(options, logger = createClackLogger()) {
39
+ const spinner = logger.spinner();
40
+ const outputDir = path.resolve(options.outputDir);
41
+ const composablesRelDir = options.composablesRelDir ?? '../use-async-data';
42
+ const runtimeRelDir = options.runtimeRelDir ?? '../runtime';
43
+ // ── 1. Analyze spec ───────────────────────────────────────────────────────
44
+ spinner.start('Analyzing OpenAPI spec');
45
+ const resourceMap = analyzeSpec(options.inputSpec);
46
+ spinner.stop(`Found ${resourceMap.size} resource(s)`);
47
+ if (resourceMap.size === 0) {
48
+ logger.log.warn('No resources found in spec — nothing to generate');
49
+ return;
50
+ }
51
+ // ── 2. Prepare output directory ───────────────────────────────────────────
52
+ // emptyDir (not ensureDir) so stale connectors from previous runs are removed.
53
+ // If the resource got renamed in the spec the old file would otherwise linger.
54
+ spinner.start('Preparing output directory');
55
+ await fs.emptyDir(outputDir);
56
+ spinner.stop('Output directory ready');
57
+ // ── 3. Generate connector files ───────────────────────────────────────────
58
+ spinner.start('Generating connector composables');
59
+ let successCount = 0;
60
+ let errorCount = 0;
61
+ const generatedNames = [];
62
+ for (const resource of resourceMap.values()) {
63
+ try {
64
+ const code = generateConnectorFile(resource, composablesRelDir);
65
+ const formatted = await formatCode(code, logger);
66
+ const fileName = connectorFileName(resource.composableName);
67
+ const filePath = path.join(outputDir, fileName);
68
+ await fs.writeFile(filePath, formatted, 'utf-8');
69
+ generatedNames.push(resource.composableName);
70
+ successCount++;
71
+ }
72
+ catch (error) {
73
+ logger.log.error(`Error generating ${resource.composableName}: ${String(error)}`);
74
+ errorCount++;
75
+ }
76
+ }
77
+ spinner.stop(`Generated ${successCount} connector(s)`);
78
+ // ── 4. Write barrel index ─────────────────────────────────────────────────
79
+ if (generatedNames.length > 0) {
80
+ try {
81
+ const indexCode = generateConnectorIndexFile(generatedNames);
82
+ const formattedIndex = await formatCode(indexCode, logger);
83
+ await fs.writeFile(path.join(outputDir, 'index.ts'), formattedIndex, 'utf-8');
84
+ }
85
+ catch (error) {
86
+ logger.log.warn(`Could not write connector index: ${String(error)}`);
87
+ }
88
+ }
89
+ // ── 5. Copy runtime helpers ───────────────────────────────────────────────
90
+ // Runtime files (useListConnector, useFormConnector …) live in src/ and must
91
+ // be physical .ts files in the user's project so Nuxt/Vite can type-check them.
92
+ //
93
+ // Path resolution trick:
94
+ // • During development (ts-node / tsx): __dirname ≈ src/generators/components/connector-generator/
95
+ // • After `tsc` build: __dirname ≈ dist/generators/components/connector-generator/
96
+ //
97
+ // In both cases we need to land in src/generators/shared/runtime/, so we step
98
+ // up 4 levels and then re-enter src/ explicitly. This works because the repo
99
+ // always keeps src/ alongside dist/ in the published package (see "files" in package.json).
100
+ spinner.start('Copying runtime files');
101
+ const runtimeDir = path.resolve(outputDir, runtimeRelDir);
102
+ await fs.ensureDir(runtimeDir); // ensureDir (not emptyDir) — other runtime files may exist there
103
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
104
+ const runtimeSrcDir = path.resolve(__dirname, '../../../../src/generators/shared/runtime');
105
+ for (const file of RUNTIME_FILES) {
106
+ const src = path.join(runtimeSrcDir, file);
107
+ const dest = path.join(runtimeDir, file);
108
+ await fs.copyFile(src, dest);
109
+ }
110
+ spinner.stop('Runtime files copied');
111
+ // ── 6. Summary ────────────────────────────────────────────────────────────
112
+ if (errorCount > 0) {
113
+ logger.log.warn(`Completed with ${errorCount} error(s)`);
114
+ }
115
+ logger.log.success(`Generated ${successCount} connector(s) in ${outputDir}`);
116
+ }
@@ -0,0 +1,18 @@
1
+ import type { ResourceInfo } from '../schema-analyzer/types.js';
2
+ /**
3
+ * Generate the full source of a `use{Resource}Connector.ts` file.
4
+ *
5
+ * @param resource ResourceInfo produced by Schema Analyzer
6
+ * @param composablesRelDir Relative path from the connector dir to the
7
+ * useAsyncData composables dir (e.g. '../use-async-data')
8
+ */
9
+ export declare function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string): string;
10
+ /**
11
+ * Derive the output filename for a connector.
12
+ * 'usePetsConnector' → 'use-pets-connector.ts'
13
+ */
14
+ export declare function connectorFileName(composableName: string): string;
15
+ /**
16
+ * Generate an index barrel file that re-exports all connectors.
17
+ */
18
+ export declare function generateConnectorIndexFile(composableNames: string[]): string;
@@ -0,0 +1,222 @@
1
+ import { pascalCase, kebabCase } from 'change-case';
2
+ // ─── File header ──────────────────────────────────────────────────────────────
3
+ function generateFileHeader() {
4
+ return `/**
5
+ * ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
6
+ *
7
+ * This file was automatically generated by nuxt-openapi-generator.
8
+ * Any manual changes will be overwritten on the next generation.
9
+ *
10
+ * @generated by nuxt-openapi-generator
11
+ * @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
12
+ */
13
+
14
+ /* eslint-disable */
15
+ // @ts-nocheck
16
+ `;
17
+ }
18
+ // ─── Naming helpers ───────────────────────────────────────────────────────────
19
+ /**
20
+ * operationId → useAsyncData composable name.
21
+ * 'getPets' → 'useAsyncDataGetPets'
22
+ */
23
+ function toAsyncDataName(operationId) {
24
+ return `useAsyncData${pascalCase(operationId)}`;
25
+ }
26
+ /**
27
+ * composable name → kebab-case file name (without .ts).
28
+ * 'useAsyncDataGetPets' → 'use-async-data-get-pets'
29
+ */
30
+ function toFileName(composableName) {
31
+ return kebabCase(composableName);
32
+ }
33
+ // ─── Section builders ─────────────────────────────────────────────────────────
34
+ /**
35
+ * Build all `import` lines for a resource connector.
36
+ */
37
+ function buildImports(resource, composablesRelDir) {
38
+ const lines = [];
39
+ // zod
40
+ lines.push(`import { z } from 'zod';`);
41
+ lines.push('');
42
+ // runtime helpers (Nuxt alias — set up by the Nuxt module)
43
+ const runtimeHelpers = [];
44
+ if (resource.listEndpoint) {
45
+ runtimeHelpers.push('useListConnector');
46
+ }
47
+ if (resource.detailEndpoint) {
48
+ runtimeHelpers.push('useDetailConnector');
49
+ }
50
+ if (resource.createEndpoint || resource.updateEndpoint) {
51
+ runtimeHelpers.push('useFormConnector');
52
+ }
53
+ if (resource.deleteEndpoint) {
54
+ runtimeHelpers.push('useDeleteConnector');
55
+ }
56
+ for (const helper of runtimeHelpers) {
57
+ lines.push(`import { ${helper} } from '#nxh/runtime/${helper}';`);
58
+ }
59
+ lines.push('');
60
+ // generated useAsyncData composables
61
+ const addImport = (operationId) => {
62
+ const name = toAsyncDataName(operationId);
63
+ const file = toFileName(name);
64
+ lines.push(`import { ${name} } from '${composablesRelDir}/${file}';`);
65
+ };
66
+ if (resource.listEndpoint) {
67
+ addImport(resource.listEndpoint.operationId);
68
+ }
69
+ if (resource.detailEndpoint) {
70
+ addImport(resource.detailEndpoint.operationId);
71
+ }
72
+ if (resource.createEndpoint) {
73
+ addImport(resource.createEndpoint.operationId);
74
+ }
75
+ if (resource.updateEndpoint) {
76
+ addImport(resource.updateEndpoint.operationId);
77
+ }
78
+ if (resource.deleteEndpoint) {
79
+ addImport(resource.deleteEndpoint.operationId);
80
+ }
81
+ return lines.join('\n');
82
+ }
83
+ /**
84
+ * Build Zod schema const declarations.
85
+ */
86
+ function buildZodSchemas(resource) {
87
+ const lines = [];
88
+ const pascal = pascalCase(resource.name);
89
+ if (resource.zodSchemas.create) {
90
+ lines.push(`const ${pascal}CreateSchema = ${resource.zodSchemas.create};`);
91
+ lines.push('');
92
+ }
93
+ if (resource.zodSchemas.update) {
94
+ lines.push(`const ${pascal}UpdateSchema = ${resource.zodSchemas.update};`);
95
+ lines.push('');
96
+ }
97
+ // Derive TS types via z.infer
98
+ if (resource.zodSchemas.create) {
99
+ lines.push(`type ${pascal}CreateInput = z.infer<typeof ${pascal}CreateSchema>;`);
100
+ }
101
+ if (resource.zodSchemas.update) {
102
+ lines.push(`type ${pascal}UpdateInput = z.infer<typeof ${pascal}UpdateSchema>;`);
103
+ }
104
+ return lines.join('\n');
105
+ }
106
+ /**
107
+ * Build the body of the exported connector function.
108
+ */
109
+ function buildFunctionBody(resource) {
110
+ const pascal = pascalCase(resource.name);
111
+ const subConnectors = [];
112
+ if (resource.listEndpoint) {
113
+ const fn = toAsyncDataName(resource.listEndpoint.operationId);
114
+ // paginated: true tells useListConnector to expose pagination helpers
115
+ // (goToPage, nextPage, prevPage, setPerPage, pagination ref).
116
+ // We set it whenever the spec declares a list endpoint that has a response schema,
117
+ // which is a reliable proxy for "this API returns structured data worth paginating".
118
+ const opts = resource.listEndpoint.responseSchema ? '{ paginated: true }' : '{}';
119
+ subConnectors.push(` const table = useListConnector(${fn}, ${opts});`);
120
+ }
121
+ if (resource.detailEndpoint) {
122
+ const fn = toAsyncDataName(resource.detailEndpoint.operationId);
123
+ subConnectors.push(` const detail = useDetailConnector(${fn});`);
124
+ }
125
+ if (resource.createEndpoint) {
126
+ const fn = toAsyncDataName(resource.createEndpoint.operationId);
127
+ const schemaArg = resource.zodSchemas.create ? `{ schema: ${pascal}CreateSchema }` : '{}';
128
+ subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg});`);
129
+ }
130
+ if (resource.updateEndpoint) {
131
+ const fn = toAsyncDataName(resource.updateEndpoint.operationId);
132
+ const hasDetail = !!resource.detailEndpoint;
133
+ // Build the options argument for useFormConnector:
134
+ // schema → Zod schema for client-side validation before submission
135
+ // loadWith → reference to the detail connector so the form auto-fills
136
+ // when detail.item changes (user clicks "Edit" on a row)
137
+ //
138
+ // Four combinations are possible depending on what the spec provides:
139
+ let schemaArg = '{}';
140
+ if (resource.zodSchemas.update && hasDetail) {
141
+ // Best case: validate AND pre-fill from detail
142
+ schemaArg = `{ schema: ${pascal}UpdateSchema, loadWith: detail }`;
143
+ }
144
+ else if (resource.zodSchemas.update) {
145
+ // Validate, but no detail endpoint to pre-fill from
146
+ schemaArg = `{ schema: ${pascal}UpdateSchema }`;
147
+ }
148
+ else if (hasDetail) {
149
+ // No Zod schema (no request body in spec), but still pre-fill from detail
150
+ schemaArg = `{ loadWith: detail }`;
151
+ }
152
+ subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg});`);
153
+ }
154
+ if (resource.deleteEndpoint) {
155
+ const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
156
+ subConnectors.push(` const deleteAction = useDeleteConnector(${fn});`);
157
+ }
158
+ // Return object — only include what was built
159
+ const returnKeys = [];
160
+ if (resource.listEndpoint) {
161
+ returnKeys.push('table');
162
+ }
163
+ if (resource.detailEndpoint) {
164
+ returnKeys.push('detail');
165
+ }
166
+ if (resource.createEndpoint) {
167
+ returnKeys.push('createForm');
168
+ }
169
+ if (resource.updateEndpoint) {
170
+ returnKeys.push('updateForm');
171
+ }
172
+ if (resource.deleteEndpoint) {
173
+ returnKeys.push('deleteAction');
174
+ }
175
+ const returnStatement = ` return { ${returnKeys.join(', ')} };`;
176
+ return [
177
+ `export function ${resource.composableName}() {`,
178
+ ...subConnectors,
179
+ returnStatement,
180
+ `}`,
181
+ ].join('\n');
182
+ }
183
+ // ─── Public API ───────────────────────────────────────────────────────────────
184
+ /**
185
+ * Generate the full source of a `use{Resource}Connector.ts` file.
186
+ *
187
+ * @param resource ResourceInfo produced by Schema Analyzer
188
+ * @param composablesRelDir Relative path from the connector dir to the
189
+ * useAsyncData composables dir (e.g. '../use-async-data')
190
+ */
191
+ export function generateConnectorFile(resource, composablesRelDir) {
192
+ const header = generateFileHeader();
193
+ const imports = buildImports(resource, composablesRelDir);
194
+ const schemas = buildZodSchemas(resource);
195
+ const fn = buildFunctionBody(resource);
196
+ // Assemble file: header + imports + (optional) Zod blocks + function body.
197
+ // Each section ends with its own trailing newline; join with \n adds one blank
198
+ // line between sections, which matches Prettier's output for this structure.
199
+ const parts = [header, imports];
200
+ if (schemas.trim()) {
201
+ parts.push(schemas);
202
+ }
203
+ parts.push(fn);
204
+ return parts.join('\n') + '\n';
205
+ }
206
+ /**
207
+ * Derive the output filename for a connector.
208
+ * 'usePetsConnector' → 'use-pets-connector.ts'
209
+ */
210
+ export function connectorFileName(composableName) {
211
+ return `${kebabCase(composableName)}.ts`;
212
+ }
213
+ /**
214
+ * Generate an index barrel file that re-exports all connectors.
215
+ */
216
+ export function generateConnectorIndexFile(composableNames) {
217
+ const header = generateFileHeader();
218
+ const exports = composableNames
219
+ .map((name) => `export { ${name} } from './${kebabCase(name)}';`)
220
+ .join('\n');
221
+ return `${header}${exports}\n`;
222
+ }
@@ -0,0 +1,32 @@
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
+ export interface ConnectorGeneratorOptions {
8
+ /** Absolute or relative path to the OpenAPI YAML/JSON spec */
9
+ inputSpec: string;
10
+ /** Directory where connector files will be written. E.g. ./composables/connectors */
11
+ outputDir: string;
12
+ /**
13
+ * Directory where the useAsyncData composables live, expressed as a path
14
+ * relative to outputDir. Defaults to '../use-async-data'.
15
+ */
16
+ composablesRelDir?: string;
17
+ /**
18
+ * Directory where runtime helpers will be copied to, expressed relative to
19
+ * outputDir. Defaults to '../runtime'.
20
+ */
21
+ runtimeRelDir?: string;
22
+ }
23
+ export interface ConnectorFileInfo {
24
+ /** PascalCase resource name. E.g. 'Pet' */
25
+ resourceName: string;
26
+ /** Generated composable function name. E.g. 'usePetsConnector' */
27
+ composableName: string;
28
+ /** Output filename (kebab-case). E.g. 'use-pets-connector.ts' */
29
+ fileName: string;
30
+ /** Formatted TypeScript source ready to be written to disk */
31
+ content: string;
32
+ }
@@ -0,0 +1,7 @@
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
+ export {};
@@ -0,0 +1,17 @@
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
+ export { readOpenApiSpec } from './openapi-reader.js';
9
+ export { detectIntent, extractEndpoints } from './intent-detector.js';
10
+ export { buildResourceMap } from './resource-grouper.js';
11
+ export { mapFieldsFromSchema, mapColumnsFromSchema, buildZodSchema, zodExpressionFromProp, } from './schema-field-mapper.js';
12
+ export type { OpenApiSpec, OpenApiSchema, OpenApiPropertySchema, OpenApiOperation, OpenApiParameter, EndpointInfo, ResourceInfo, ResourceMap, FormFieldDef, ColumnDef, Intent, FieldType, ColumnType, } from './types.js';
13
+ import type { ResourceMap } from './types.js';
14
+ /**
15
+ * Convenience function: read a spec file and return the full ResourceMap.
16
+ */
17
+ export declare function analyzeSpec(specPath: string): ResourceMap;
@@ -0,0 +1,20 @@
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
+ export { readOpenApiSpec } from './openapi-reader.js';
9
+ export { detectIntent, extractEndpoints } from './intent-detector.js';
10
+ export { buildResourceMap } from './resource-grouper.js';
11
+ export { mapFieldsFromSchema, mapColumnsFromSchema, buildZodSchema, zodExpressionFromProp, } from './schema-field-mapper.js';
12
+ import { readOpenApiSpec } from './openapi-reader.js';
13
+ import { buildResourceMap } from './resource-grouper.js';
14
+ /**
15
+ * Convenience function: read a spec file and return the full ResourceMap.
16
+ */
17
+ export function analyzeSpec(specPath) {
18
+ const spec = readOpenApiSpec(specPath);
19
+ return buildResourceMap(spec);
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { EndpointInfo, Intent, OpenApiOperation } from './types.js';
2
+ declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
3
+ type HttpMethod = (typeof HTTP_METHODS)[number];
4
+ /**
5
+ * Detect the CRUD intent of a single endpoint.
6
+ *
7
+ * Priority:
8
+ * 1. x-nxh-intent extension on the operation (developer override)
9
+ * 2. HTTP method + path pattern + response schema
10
+ */
11
+ export declare function detectIntent(method: HttpMethod, path: string, operation: OpenApiOperation): Intent;
12
+ /**
13
+ * Extract all endpoints from a single path item as EndpointInfo[].
14
+ * The spec must already be $ref-resolved before calling this.
15
+ */
16
+ export declare function extractEndpoints(path: string, pathItem: Record<string, OpenApiOperation>): EndpointInfo[];
17
+ export {};
@@ -0,0 +1,143 @@
1
+ // HTTP methods we care about
2
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH']);
3
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
4
+ // ─── Path analysis helpers ────────────────────────────────────────────────────
5
+ /** Returns path parameter names found in a path, e.g. '/pets/{id}' → ['id'] */
6
+ function extractPathParams(path) {
7
+ const matches = path.match(/\{([^}]+)\}/g) ?? [];
8
+ return matches.map((m) => m.slice(1, -1));
9
+ }
10
+ /** True when the path ends with a path parameter: /pets/{id} */
11
+ function endsWithPathParam(path) {
12
+ return /\/\{[^}]+\}$/.test(path);
13
+ }
14
+ // ─── Response schema analysis ─────────────────────────────────────────────────
15
+ /**
16
+ * Return the resolved schema for the first 2xx response that has
17
+ * an application/json body, or undefined.
18
+ */
19
+ function getSuccessResponseSchema(operation) {
20
+ if (!operation.responses) {
21
+ return undefined;
22
+ }
23
+ for (const [statusCode, response] of Object.entries(operation.responses)) {
24
+ const code = parseInt(statusCode, 10);
25
+ if (isNaN(code) || code < 200 || code >= 300) {
26
+ continue;
27
+ }
28
+ const jsonContent = response.content?.['application/json'];
29
+ if (jsonContent?.schema) {
30
+ return jsonContent.schema;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ /** True when schema represents an array (type: array, or items present) */
36
+ function isArraySchema(schema) {
37
+ return schema.type === 'array' || schema.items !== undefined;
38
+ }
39
+ // ─── Request body schema ──────────────────────────────────────────────────────
40
+ function getRequestBodySchema(operation) {
41
+ if (!operation.requestBody?.content) {
42
+ return undefined;
43
+ }
44
+ const jsonContent = operation.requestBody.content['application/json'];
45
+ if (jsonContent?.schema) {
46
+ return jsonContent.schema;
47
+ }
48
+ // Fallback to form-urlencoded
49
+ const formContent = operation.requestBody.content['application/x-www-form-urlencoded'];
50
+ return formContent?.schema;
51
+ }
52
+ // ─── Intent detection ─────────────────────────────────────────────────────────
53
+ /**
54
+ * Detect the CRUD intent of a single endpoint.
55
+ *
56
+ * Priority:
57
+ * 1. x-nxh-intent extension on the operation (developer override)
58
+ * 2. HTTP method + path pattern + response schema
59
+ */
60
+ export function detectIntent(method, path, operation) {
61
+ // 1. Developer override via OpenAPI extension
62
+ const override = operation['x-nxh-intent'];
63
+ if (override) {
64
+ return override;
65
+ }
66
+ const hasPathParam = extractPathParams(path).length > 0;
67
+ const responseSchema = getSuccessResponseSchema(operation);
68
+ switch (method) {
69
+ case 'DELETE':
70
+ return 'delete';
71
+ case 'POST':
72
+ // POST /resource → create
73
+ // POST /resource/{id}/action → unknown (custom action, not CRUD)
74
+ return !endsWithPathParam(path) ? 'create' : 'unknown';
75
+ case 'PUT':
76
+ case 'PATCH':
77
+ return 'update';
78
+ case 'GET': {
79
+ // A GET without a JSON response (e.g. binary download) is not a CRUD intent
80
+ if (!responseSchema) {
81
+ return 'unknown';
82
+ }
83
+ // Array response ( type: 'array' OR has 'items' ) → always a list
84
+ if (isArraySchema(responseSchema)) {
85
+ return 'list';
86
+ }
87
+ // Object response — distinguish list vs detail by path structure:
88
+ // GET /pets/{id} → has path param → detail (single item fetch)
89
+ // GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
90
+ if (hasPathParam) {
91
+ return 'detail';
92
+ }
93
+ return 'list';
94
+ }
95
+ default:
96
+ return 'unknown';
97
+ }
98
+ }
99
+ // ─── Endpoint extraction ──────────────────────────────────────────────────────
100
+ /**
101
+ * Extract all endpoints from a single path item as EndpointInfo[].
102
+ * The spec must already be $ref-resolved before calling this.
103
+ */
104
+ export function extractEndpoints(path, pathItem) {
105
+ const results = [];
106
+ const pathParams = extractPathParams(path);
107
+ for (const method of HTTP_METHODS) {
108
+ const operation = pathItem[method.toLowerCase()];
109
+ if (!operation) {
110
+ continue;
111
+ }
112
+ const intent = detectIntent(method, path, operation);
113
+ const endpoint = {
114
+ // Fallback operationId when the spec omits it: 'get_/pets/{id}' → 'get__pets__id_'
115
+ // This rarely produces a ideal composable name, but avoids a crash.
116
+ operationId: operation.operationId ?? `${method.toLowerCase()}_${path.replace(/\//g, '_')}`,
117
+ method,
118
+ path,
119
+ tags: operation.tags ?? [],
120
+ summary: operation.summary,
121
+ description: operation.description,
122
+ intent,
123
+ hasPathParams: pathParams.length > 0,
124
+ pathParams,
125
+ };
126
+ // Attach response schema for GET intents
127
+ if (method === 'GET') {
128
+ const schema = getSuccessResponseSchema(operation);
129
+ if (schema) {
130
+ endpoint.responseSchema = schema;
131
+ }
132
+ }
133
+ // Attach request body schema for mutating methods
134
+ if (MUTATING_METHODS.has(method)) {
135
+ const schema = getRequestBodySchema(operation);
136
+ if (schema) {
137
+ endpoint.requestBodySchema = schema;
138
+ }
139
+ }
140
+ results.push(endpoint);
141
+ }
142
+ return results;
143
+ }
@@ -0,0 +1,11 @@
1
+ import type { OpenApiSpec, OpenApiPropertySchema } from './types.js';
2
+ /**
3
+ * Read an OpenAPI spec from a YAML or JSON file.
4
+ * Returns the parsed spec with all $ref values resolved inline.
5
+ */
6
+ export declare function readOpenApiSpec(filePath: string): OpenApiSpec;
7
+ /**
8
+ * Resolve a single inline schema that may still have $ref (convenience helper
9
+ * used by other modules that receive already-partially-resolved specs).
10
+ */
11
+ export declare function resolveSchema(schema: OpenApiPropertySchema, root: OpenApiSpec): OpenApiPropertySchema;