nuxt-openapi-hyperfetch 0.3.1-beta → 0.3.3-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.
- package/dist/generators/components/connector-generator/generator.js +1 -1
- package/dist/generators/components/connector-generator/templates.d.ts +1 -1
- package/dist/generators/components/connector-generator/templates.js +6 -6
- package/dist/generators/components/schema-analyzer/intent-detector.js +12 -0
- package/dist/generators/components/schema-analyzer/schema-field-mapper.js +22 -1
- package/dist/index.js +13 -0
- package/dist/module/index.js +0 -3
- package/package.json +1 -1
- package/src/generators/components/connector-generator/generator.ts +1 -1
- package/src/generators/components/connector-generator/templates.ts +7 -6
- package/src/generators/components/schema-analyzer/intent-detector.ts +16 -0
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +26 -1
- package/src/index.ts +15 -0
- package/src/module/index.ts +0 -3
|
@@ -62,7 +62,7 @@ export async function generateConnectors(options, logger = createClackLogger())
|
|
|
62
62
|
const generatedNames = [];
|
|
63
63
|
for (const resource of resourceMap.values()) {
|
|
64
64
|
try {
|
|
65
|
-
const code = generateConnectorFile(resource, composablesRelDir);
|
|
65
|
+
const code = generateConnectorFile(resource, composablesRelDir, '../..', runtimeRelDir);
|
|
66
66
|
const formatted = await formatCode(code, logger);
|
|
67
67
|
const fileName = connectorFileName(resource.composableName);
|
|
68
68
|
const filePath = path.join(outputDir, fileName);
|
|
@@ -6,7 +6,7 @@ import type { ResourceInfo } from '../schema-analyzer/types.js';
|
|
|
6
6
|
* @param composablesRelDir Relative path from the connector dir to the
|
|
7
7
|
* useAsyncData composables dir (e.g. '../use-async-data')
|
|
8
8
|
*/
|
|
9
|
-
export declare function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string, sdkRelDir?: string): string;
|
|
9
|
+
export declare function generateConnectorFile(resource: ResourceInfo, composablesRelDir: string, sdkRelDir?: string, runtimeRelDir?: string): string;
|
|
10
10
|
/**
|
|
11
11
|
* Derive the output filename for a connector.
|
|
12
12
|
* 'usePetsConnector' → 'use-pets-connector.ts'
|
|
@@ -33,7 +33,7 @@ function toFileName(composableName) {
|
|
|
33
33
|
/**
|
|
34
34
|
* Build all `import` lines for a resource connector.
|
|
35
35
|
*/
|
|
36
|
-
function buildImports(resource, composablesRelDir, sdkRelDir) {
|
|
36
|
+
function buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir) {
|
|
37
37
|
const lines = [];
|
|
38
38
|
// zod
|
|
39
39
|
lines.push(`import { z } from 'zod';`);
|
|
@@ -49,7 +49,7 @@ function buildImports(resource, composablesRelDir, sdkRelDir) {
|
|
|
49
49
|
if (resource.deleteEndpoint) {
|
|
50
50
|
connectorTypeImports.push('DeleteConnectorReturn');
|
|
51
51
|
}
|
|
52
|
-
lines.push(`import type { ${connectorTypeImports.join(', ')} } from '
|
|
52
|
+
lines.push(`import type { ${connectorTypeImports.join(', ')} } from '${runtimeRelDir}/connector-types';`);
|
|
53
53
|
lines.push('');
|
|
54
54
|
// SDK request/response types (for the params overload signature)
|
|
55
55
|
if (resource.listEndpoint) {
|
|
@@ -57,7 +57,7 @@ function buildImports(resource, composablesRelDir, sdkRelDir) {
|
|
|
57
57
|
lines.push(`import type { ${requestTypeName} } from '${sdkRelDir}';`);
|
|
58
58
|
lines.push('');
|
|
59
59
|
}
|
|
60
|
-
// runtime helpers
|
|
60
|
+
// runtime helpers — relative path works in both CLI and Nuxt module contexts
|
|
61
61
|
// useListConnector is always imported to support the optional factory pattern
|
|
62
62
|
const runtimeHelpers = ['useListConnector'];
|
|
63
63
|
if (resource.detailEndpoint) {
|
|
@@ -70,7 +70,7 @@ function buildImports(resource, composablesRelDir, sdkRelDir) {
|
|
|
70
70
|
runtimeHelpers.push('useDeleteConnector');
|
|
71
71
|
}
|
|
72
72
|
for (const helper of runtimeHelpers) {
|
|
73
|
-
lines.push(`import { ${helper} } from '
|
|
73
|
+
lines.push(`import { ${helper} } from '${runtimeRelDir}/${helper}';`);
|
|
74
74
|
}
|
|
75
75
|
lines.push('');
|
|
76
76
|
// generated useAsyncData composables
|
|
@@ -312,9 +312,9 @@ function buildFunctionBody(resource) {
|
|
|
312
312
|
* @param composablesRelDir Relative path from the connector dir to the
|
|
313
313
|
* useAsyncData composables dir (e.g. '../use-async-data')
|
|
314
314
|
*/
|
|
315
|
-
export function generateConnectorFile(resource, composablesRelDir, sdkRelDir = '../..') {
|
|
315
|
+
export function generateConnectorFile(resource, composablesRelDir, sdkRelDir = '../..', runtimeRelDir = '../../runtime') {
|
|
316
316
|
const header = generateFileHeader();
|
|
317
|
-
const imports = buildImports(resource, composablesRelDir, sdkRelDir);
|
|
317
|
+
const imports = buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir);
|
|
318
318
|
const schemas = buildZodSchemas(resource);
|
|
319
319
|
const columns = buildColumns(resource);
|
|
320
320
|
const optionsInterface = buildOptionsInterface(resource);
|
|
@@ -36,6 +36,13 @@ function getSuccessResponseSchema(operation) {
|
|
|
36
36
|
function isArraySchema(schema) {
|
|
37
37
|
return schema.type === 'array' || schema.items !== undefined;
|
|
38
38
|
}
|
|
39
|
+
/** True when schema is a primitive scalar (string, number, integer, boolean) — not a resource */
|
|
40
|
+
function isPrimitiveSchema(schema) {
|
|
41
|
+
return (schema.type === 'string' ||
|
|
42
|
+
schema.type === 'number' ||
|
|
43
|
+
schema.type === 'integer' ||
|
|
44
|
+
schema.type === 'boolean');
|
|
45
|
+
}
|
|
39
46
|
// ─── Request body schema ──────────────────────────────────────────────────────
|
|
40
47
|
function getRequestBodySchema(operation) {
|
|
41
48
|
if (!operation.requestBody?.content) {
|
|
@@ -84,6 +91,11 @@ export function detectIntent(method, path, operation) {
|
|
|
84
91
|
if (isArraySchema(responseSchema)) {
|
|
85
92
|
return 'list';
|
|
86
93
|
}
|
|
94
|
+
// Primitive response (string, number, boolean) → not a CRUD resource
|
|
95
|
+
// e.g. GET /user/login returns a string token — not a list or detail
|
|
96
|
+
if (isPrimitiveSchema(responseSchema)) {
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
87
99
|
// Object response — distinguish list vs detail by path structure:
|
|
88
100
|
// GET /pets/{id} → has path param → detail (single item fetch)
|
|
89
101
|
// GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
|
|
@@ -132,7 +132,7 @@ function baseZodExpr(prop) {
|
|
|
132
132
|
case 'array':
|
|
133
133
|
return arrayZodExpr(prop);
|
|
134
134
|
case 'object':
|
|
135
|
-
return
|
|
135
|
+
return objectZodExpr(prop);
|
|
136
136
|
default:
|
|
137
137
|
// $ref already resolved, unknown type → permissive
|
|
138
138
|
return 'z.unknown()';
|
|
@@ -181,6 +181,27 @@ function numberZodExpr(prop) {
|
|
|
181
181
|
}
|
|
182
182
|
return expr;
|
|
183
183
|
}
|
|
184
|
+
function objectZodExpr(prop) {
|
|
185
|
+
const { additionalProperties } = prop;
|
|
186
|
+
// additionalProperties: false or undefined → plain object
|
|
187
|
+
if (!additionalProperties || additionalProperties === true) {
|
|
188
|
+
return 'z.record(z.unknown())';
|
|
189
|
+
}
|
|
190
|
+
// additionalProperties has a known primitive type → typed record
|
|
191
|
+
const valueExpr = additionalPropsZodExpr(additionalProperties);
|
|
192
|
+
return `z.record(${valueExpr})`;
|
|
193
|
+
}
|
|
194
|
+
function additionalPropsZodExpr(schema) {
|
|
195
|
+
switch (schema.type) {
|
|
196
|
+
case 'string': return stringZodExpr(schema);
|
|
197
|
+
case 'integer': return integerZodExpr(schema);
|
|
198
|
+
case 'number': return numberZodExpr(schema);
|
|
199
|
+
case 'boolean': return 'z.boolean()';
|
|
200
|
+
case 'array': return arrayZodExpr(schema);
|
|
201
|
+
case 'object': return objectZodExpr(schema);
|
|
202
|
+
default: return 'z.unknown()';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
184
205
|
function arrayZodExpr(prop) {
|
|
185
206
|
const itemExpr = prop.items ? baseZodExpr(prop.items) : 'z.unknown()';
|
|
186
207
|
let expr = `z.array(${itemExpr})`;
|
package/dist/index.js
CHANGED
|
@@ -196,6 +196,19 @@ program
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
// Generate headless connectors if requested (requires useAsyncData)
|
|
199
|
+
if (generateConnectorsFlag) {
|
|
200
|
+
// Check zod is available in the user's project before attempting generation
|
|
201
|
+
try {
|
|
202
|
+
// Use a variable to prevent TypeScript from resolving the module at compile time
|
|
203
|
+
const zodId = 'zod';
|
|
204
|
+
await import(zodId);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
p.log.warn('Skipping connectors: "zod" is not installed in this project.\n' +
|
|
208
|
+
' Run: npm install zod');
|
|
209
|
+
generateConnectorsFlag = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
199
212
|
if (generateConnectorsFlag) {
|
|
200
213
|
const spinner = p.spinner();
|
|
201
214
|
spinner.start('Generating headless UI connectors...');
|
package/dist/module/index.js
CHANGED
|
@@ -70,15 +70,12 @@ export default defineNuxtModule({
|
|
|
70
70
|
if (options.createUseAsyncDataConnectors &&
|
|
71
71
|
selectedGenerators.includes('useAsyncData')) {
|
|
72
72
|
const connectorsOutputDir = path.join(composablesOutputDir, 'connectors');
|
|
73
|
-
const runtimeDir = path.join(resolvedOutput, 'runtime');
|
|
74
73
|
await generateConnectors({
|
|
75
74
|
inputSpec: resolvedInput,
|
|
76
75
|
outputDir: connectorsOutputDir,
|
|
77
76
|
composablesRelDir: '../use-async-data',
|
|
78
77
|
runtimeRelDir: '../../runtime',
|
|
79
78
|
}, logger);
|
|
80
|
-
// Register #nxh alias so generated connector imports resolve
|
|
81
|
-
nuxt.options.alias['#nxh'] = runtimeDir;
|
|
82
79
|
}
|
|
83
80
|
};
|
|
84
81
|
// --- Hooks: dev build / production build ---
|
package/package.json
CHANGED
|
@@ -78,7 +78,7 @@ export async function generateConnectors(
|
|
|
78
78
|
|
|
79
79
|
for (const resource of resourceMap.values()) {
|
|
80
80
|
try {
|
|
81
|
-
const code = generateConnectorFile(resource, composablesRelDir);
|
|
81
|
+
const code = generateConnectorFile(resource, composablesRelDir, '../..', runtimeRelDir);
|
|
82
82
|
const formatted = await formatCode(code, logger);
|
|
83
83
|
const fileName = connectorFileName(resource.composableName);
|
|
84
84
|
const filePath = path.join(outputDir, fileName);
|
|
@@ -41,7 +41,7 @@ function toFileName(composableName: string): string {
|
|
|
41
41
|
/**
|
|
42
42
|
* Build all `import` lines for a resource connector.
|
|
43
43
|
*/
|
|
44
|
-
function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelDir: string): string {
|
|
44
|
+
function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelDir: string, runtimeRelDir: string): string {
|
|
45
45
|
const lines: string[] = [];
|
|
46
46
|
|
|
47
47
|
// zod
|
|
@@ -59,7 +59,7 @@ function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelD
|
|
|
59
59
|
if (resource.deleteEndpoint) {
|
|
60
60
|
connectorTypeImports.push('DeleteConnectorReturn');
|
|
61
61
|
}
|
|
62
|
-
lines.push(`import type { ${connectorTypeImports.join(', ')} } from '
|
|
62
|
+
lines.push(`import type { ${connectorTypeImports.join(', ')} } from '${runtimeRelDir}/connector-types';`);
|
|
63
63
|
lines.push('');
|
|
64
64
|
|
|
65
65
|
// SDK request/response types (for the params overload signature)
|
|
@@ -69,7 +69,7 @@ function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelD
|
|
|
69
69
|
lines.push('');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// runtime helpers
|
|
72
|
+
// runtime helpers — relative path works in both CLI and Nuxt module contexts
|
|
73
73
|
// useListConnector is always imported to support the optional factory pattern
|
|
74
74
|
const runtimeHelpers: string[] = ['useListConnector'];
|
|
75
75
|
if (resource.detailEndpoint) {
|
|
@@ -83,7 +83,7 @@ function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelD
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
for (const helper of runtimeHelpers) {
|
|
86
|
-
lines.push(`import { ${helper} } from '
|
|
86
|
+
lines.push(`import { ${helper} } from '${runtimeRelDir}/${helper}';`);
|
|
87
87
|
}
|
|
88
88
|
lines.push('');
|
|
89
89
|
|
|
@@ -375,10 +375,11 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
375
375
|
export function generateConnectorFile(
|
|
376
376
|
resource: ResourceInfo,
|
|
377
377
|
composablesRelDir: string,
|
|
378
|
-
sdkRelDir = '../..'
|
|
378
|
+
sdkRelDir = '../..',
|
|
379
|
+
runtimeRelDir = '../../runtime'
|
|
379
380
|
): string {
|
|
380
381
|
const header = generateFileHeader();
|
|
381
|
-
const imports = buildImports(resource, composablesRelDir, sdkRelDir);
|
|
382
|
+
const imports = buildImports(resource, composablesRelDir, sdkRelDir, runtimeRelDir);
|
|
382
383
|
const schemas = buildZodSchemas(resource);
|
|
383
384
|
const columns = buildColumns(resource);
|
|
384
385
|
const optionsInterface = buildOptionsInterface(resource);
|
|
@@ -49,6 +49,16 @@ function isArraySchema(schema: OpenApiPropertySchema): boolean {
|
|
|
49
49
|
return schema.type === 'array' || schema.items !== undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** True when schema is a primitive scalar (string, number, integer, boolean) — not a resource */
|
|
53
|
+
function isPrimitiveSchema(schema: OpenApiPropertySchema): boolean {
|
|
54
|
+
return (
|
|
55
|
+
schema.type === 'string' ||
|
|
56
|
+
schema.type === 'number' ||
|
|
57
|
+
schema.type === 'integer' ||
|
|
58
|
+
schema.type === 'boolean'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
// ─── Request body schema ──────────────────────────────────────────────────────
|
|
53
63
|
|
|
54
64
|
function getRequestBodySchema(operation: OpenApiOperation): OpenApiPropertySchema | undefined {
|
|
@@ -113,6 +123,12 @@ export function detectIntent(
|
|
|
113
123
|
return 'list';
|
|
114
124
|
}
|
|
115
125
|
|
|
126
|
+
// Primitive response (string, number, boolean) → not a CRUD resource
|
|
127
|
+
// e.g. GET /user/login returns a string token — not a list or detail
|
|
128
|
+
if (isPrimitiveSchema(responseSchema)) {
|
|
129
|
+
return 'unknown';
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
// Object response — distinguish list vs detail by path structure:
|
|
117
133
|
// GET /pets/{id} → has path param → detail (single item fetch)
|
|
118
134
|
// GET /pets → no path param → list (likely paginated envelope: { data: [], total: n })
|
|
@@ -167,7 +167,7 @@ function baseZodExpr(prop: OpenApiPropertySchema): string {
|
|
|
167
167
|
return arrayZodExpr(prop);
|
|
168
168
|
|
|
169
169
|
case 'object':
|
|
170
|
-
return
|
|
170
|
+
return objectZodExpr(prop);
|
|
171
171
|
|
|
172
172
|
default:
|
|
173
173
|
// $ref already resolved, unknown type → permissive
|
|
@@ -224,6 +224,31 @@ function numberZodExpr(prop: OpenApiPropertySchema): string {
|
|
|
224
224
|
return expr;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function objectZodExpr(prop: OpenApiPropertySchema): string {
|
|
228
|
+
const { additionalProperties } = prop;
|
|
229
|
+
|
|
230
|
+
// additionalProperties: false or undefined → plain object
|
|
231
|
+
if (!additionalProperties || additionalProperties === true) {
|
|
232
|
+
return 'z.record(z.unknown())';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// additionalProperties has a known primitive type → typed record
|
|
236
|
+
const valueExpr = additionalPropsZodExpr(additionalProperties);
|
|
237
|
+
return `z.record(${valueExpr})`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function additionalPropsZodExpr(schema: OpenApiPropertySchema): string {
|
|
241
|
+
switch (schema.type) {
|
|
242
|
+
case 'string': return stringZodExpr(schema);
|
|
243
|
+
case 'integer': return integerZodExpr(schema);
|
|
244
|
+
case 'number': return numberZodExpr(schema);
|
|
245
|
+
case 'boolean': return 'z.boolean()';
|
|
246
|
+
case 'array': return arrayZodExpr(schema);
|
|
247
|
+
case 'object': return objectZodExpr(schema);
|
|
248
|
+
default: return 'z.unknown()';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
227
252
|
function arrayZodExpr(prop: OpenApiPropertySchema): string {
|
|
228
253
|
const itemExpr = prop.items ? baseZodExpr(prop.items) : 'z.unknown()';
|
|
229
254
|
let expr = `z.array(${itemExpr})`;
|
package/src/index.ts
CHANGED
|
@@ -253,6 +253,21 @@ program
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
// Generate headless connectors if requested (requires useAsyncData)
|
|
256
|
+
if (generateConnectorsFlag) {
|
|
257
|
+
// Check zod is available in the user's project before attempting generation
|
|
258
|
+
try {
|
|
259
|
+
// Use a variable to prevent TypeScript from resolving the module at compile time
|
|
260
|
+
const zodId = 'zod';
|
|
261
|
+
await import(zodId);
|
|
262
|
+
} catch {
|
|
263
|
+
p.log.warn(
|
|
264
|
+
'Skipping connectors: "zod" is not installed in this project.\n' +
|
|
265
|
+
' Run: npm install zod'
|
|
266
|
+
);
|
|
267
|
+
generateConnectorsFlag = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
256
271
|
if (generateConnectorsFlag) {
|
|
257
272
|
const spinner = p.spinner();
|
|
258
273
|
spinner.start('Generating headless UI connectors...');
|
package/src/module/index.ts
CHANGED
|
@@ -108,7 +108,6 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
108
108
|
selectedGenerators.includes('useAsyncData')
|
|
109
109
|
) {
|
|
110
110
|
const connectorsOutputDir = path.join(composablesOutputDir, 'connectors');
|
|
111
|
-
const runtimeDir = path.join(resolvedOutput, 'runtime');
|
|
112
111
|
await generateConnectors(
|
|
113
112
|
{
|
|
114
113
|
inputSpec: resolvedInput,
|
|
@@ -118,8 +117,6 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
118
117
|
},
|
|
119
118
|
logger
|
|
120
119
|
);
|
|
121
|
-
// Register #nxh alias so generated connector imports resolve
|
|
122
|
-
nuxt.options.alias['#nxh'] = runtimeDir;
|
|
123
120
|
}
|
|
124
121
|
};
|
|
125
122
|
|